### 从乱码到中文:前端二维码解码的一次实操记录 #### 背景 客户那边有一套用了很多年的 ERP 系统,导出的二维码用手机扫一扫,呈现出来的是一串看不懂的
### 从乱码到中文:前端二维码解码的一次实操记录
#### 背景
客户那边有一套用了很多年的 ERP 系统,导出的二维码用手机扫一扫,呈现出来的是一串看不懂的乱码。用 Java 或者 Python 去读图片底层的字节数据,再做一次编码转换,是可以还原成正常中文的。但我们的场景是纯前端——用户在浏览器里上传图片或者打开摄像头,就要直接得到正确的结果,不能依赖后端服务。
前端这边折腾了挺久,最后用 `zxing-wasm` 配合一套自定义的解码逻辑,总算把这个问题解决了。
#### 解码引擎的选择
`zxing-wasm` 是 ZXing C++ 库的 WebAssembly 版本,编译后可以直接在浏览器里运行。调用时把图像数据传进去,通过配置参数来启用各种内置的预处理能力:
```ts
const results = await readBarcodes(imageData, {
formats: ['QRCode'],
tryRotate: true, // 自动旋转 0/90/180/270 度
tryInvert: true, // 尝试反色(黑底白字)
tryHarder: true, // 启用更精细的搜索
tryDownscale: true, // 大图自动缩小以加速
tryDenoise: true, // 形态学降噪
maxNumberOfSymbols: 4,
});
```
灰度化、二值化、旋转修正、反色判断、降噪这些步骤,全部在 C++ 层内部完成,前端不需要额外处理,代码更简洁,识别率也有保障。
#### 乱码问题的根源
引擎虽然能识别出二维码的内容,但遇到不带 ECI 字符集声明的中文二维码时,会默认按 UTF-8 解码。GBK 编码的中文在这种情况下就会显示成 `汉å—` 这样的乱码——这是字符集探测本身的局限,不是引擎的 Bug。
`readBarcodes` 返回的结果里,除了 `text`(引擎自动解码后的文本),还有一个 `bytes` 字段,存放的是原始字节数组。问题就转化成了:拿到原始字节后,如何判断它到底应该用什么编码来解读。
#### 暴力探测 + 评分择优
思路很简单:把常见的几种中文编码都试一遍,然后挑一个最像“正常文本”的结果。
```ts
function decodeBytesSmart(bytes: Uint8Array) {
const encodings = ['gbk', 'gb18030', 'gb2312', 'utf-8', 'big5'];
const attempts = encodings.map(enc => {
try {
const text = new TextDecoder(enc).decode(bytes);
return { encoding: enc, text, score: scoreText(text) };
} catch { return null; }
}).filter(Boolean);
attempts.sort((a, b) => b.score - a.score);
return attempts[0];
}
```
关键是这个评分函数——怎么判断一段文本是“正常的”还是“乱码”?
```ts
function scoreText(t: string): number {
if (!t) return -1e9;
let s = 0;
// 中文字符越多,得分越高
const cn = t.match(/[\u4e00-\u9fff]/g);
if (cn) s += cn.length * 5;
// 以 { 或 [ 开头,可能是 JSON,加分
if (t.trim().startsWith('{') || t.trim().startsWith('[')) s += 20;
// 包含典型的 UTF-8 误读乱码字符,减分
if (/Ã.|Â./.test(t)) s -= 30;
// 高字节西欧符号,减分
const garbage = (t.match(/[€‚ƒ…†‡ˆ‰Š‹ŒŽ'""•–—˜™š›œžŸ]/g) || []).length;
s -= garbage * 10;
return s;
}
```
中文字符多、有 JSON 结构,说明这段文本很大概率是正常的;出现 `Ã`、`Â` 这类 Latin-1 误读字符,或者大量高字节符号,基本就是乱码。用这个规则给每个编码尝试结果打分,分数最高的那个就是最可能的正确文本。
最后一步,把引擎自带的解码结果和暴力探测的最高分结果放在一起比较,谁分高就用谁:
```ts
const smart = decodeBytesSmart(target.bytes);
let text = target.text;
if (!text || scoreText(smart.text) > scoreText(text)) {
text = smart.text;
}
```
#### 结果
这套方案上线之后,无论是标准的 UTF-8 二维码,还是老 ERP 导出的 GBK 编码二维码,都能在浏览器里直接还原成正确的中文内容。整个过程不依赖后端,用户上传图片或打开摄像头扫码,得到的就是可读的文本。
算是给这个“老旧 ERP + 现代前端”的组合,补齐了最后一块拼图。
收起