二维码生成(QR Code)
由 Rust + WebAssembly 手写 Reed-Solomon 纠错与掩码选择,输入即生成
用手机扫一扫验证 | 纠错等级越高,二维码越大,但污损一部分也能扫出来
📖 原理说明
二维码把文字编码成黑白方格,即使被遮挡或弄脏一部分也能读出来,秘诀在于 Reed-Solomon 纠错码。这个示例按照 ISO/IEC 18004 标准从零实现了完整的二维码编码流程:数据编码、纠错码计算、分块交织、模块排布、掩码选择和格式信息,支持全部 40 个版本和 4 个纠错等级。
🧮算法原理
码字 = 数据 ‖ (数据 · xn mod g(x))Reed-Solomon:g(x) = ∏(x − αi),在 GF(2⁸) 上计算
- 数据编码
- 文字先按 UTF-8 转成字节,前面加上 4 位模式标识(0100 = 字节模式)和字节数,结尾补 4 位终止符并对齐到整字节,剩余容量用
0xEC、0x11 交替填满。选择能装下这些数据的最小版本(21×21 到 177×177)。
- Reed-Solomon 纠错
- 把数据看作有限域 GF(2⁸) 上的多项式,除以生成多项式 g(x) 得到的余数就是纠错码字。n 个纠错码字最多能纠正 n/2 个出错的码字。纠错等级 L / M / Q / H 分别能恢复约 7% / 15% / 25% / 30% 的损坏。
- 分块与交织
- 较大的版本把数据分成多个块,每块各自计算纠错码,再按列交错排列。这样一块局部污渍损坏的是分散在多个块里的少量码字,每块都还在纠错能力之内。
- 模块排布
- 先画出固定图案:三个角上的定位图案、时序线、对齐图案,版本 7 以上还有版本信息;然后把码字比特从右下角开始,两列一组,像蛇一样上下来回填进剩余格子。
- 掩码与评分
- 大片同色、类似定位图案的排列会让扫码器误判,所以要用 8 种掩码图案之一与数据区做异或。每种掩码按 4 条规则打分(连续同色、2×2 色块、假定位图案、黑白比例),选分数最低的那个,并把掩码编号和纠错等级写进两份 BCH 编码的格式信息里。
页面上的柱状图就是 8 种掩码的惩罚分,绿色是最低分,橙色是实际使用的掩码。手动指定掩码后,二维码依然能扫出来,只是可读性可能稍差。
🔄Rust 与 JavaScript 的分工
- JS输入框每次变化,JS 调用
qr_encode(文字, 纠错等级, 掩码)(掩码为 −1 表示自动选择)。
- RustRust 完成编码、纠错、排布和掩码,返回
[边长, 版本, 掩码, 模块…],每个模块 1 字节(1 = 黑)。qr_penalties() 返回 8 种掩码各自的惩罚分。
- JSJS 在四周留出 4 个模块宽的空白(标准要求的「静区」)后按整数倍放大绘制;下载时另外按 10 倍重新绘制成清晰的 PNG。
⚡性能要点
- 测试时用独立实现 segno 生成的二维码做对照:400 个随机输入(含中文和 emoji,覆盖 29 个版本、全部纠错等级和掩码)逐个模块完全一致,自动掩码生成的 120 个二维码全部能被 ZXing 解码还原。
- 对照过程中发现 segno 在比特流恰好对齐字节时会多补一个 0 字节(不符合标准但不影响扫码);两者自动选掩码的时机也不同:segno 在写入格式信息之前评分,本实现对完成后的整个符号评分(与 Nayuki、ZXing 相同),两种做法生成的二维码都有效。
- 有限域乘法用逐位移位异或实现,无需查表;即使是 177×177 的版本 40,编码一次也只需几毫秒,其中大部分时间花在 8 次掩码评分上。
源码crates/tools/qrcode/src/lib.rswww/qrcode/index.js