杂记 › 生活随笔 › 同一串 23 字节的内容切换 4 个纠错等级:这一页自己的二维码编码器走到了哪一步

同一串 23 字节的内容切换 4 个纠错等级:这一页自己的二维码编码器走到了哪一步

同一条 23 字节的内容在 L/M/Q/H 下落进 v2/v2/v3/v4,模块数 25、25、29、33;这一页自己写的编码器只有 8 位字节一条码流,容量阶梯按表值减 2,纠错码字只保证个数,掩码是一枚固定棋盘。

同一个字没改,格子从 25 个变成 33 个

同一条 23 字节的内容,纠错等级从 L 拨到 H,统计那行从「尺寸:25x25」变成「尺寸:33x33」,导出的画布却从 297×297 缩到 287×287。一个字没改,格子变密、画布反而变小,这两件事各从哪来?

这一页的编码器是自己写的:二维码生成器 模板第 158–465 行是一段从 GF(256) 表到矩阵生成的码流,不引用外部库。

一条码流:模式指示器恒 0100

第 284 行是全文件唯一的模式指示器写入 bits.push(0, 1, 0, 0),不在任何条件分支里,于是模式恒 0100、每字节恒 8 bit;纯数字、字母数字、汉字、emoji 实测都只切成 1 段,numeric、alphanumeric、KANJI 三个关键字命中 0 次。长度字段 8 bit(v10 取 16 bit),头部合计 12 或 20 bit。汉字没有单独的段:「中文测试」4 字符 12 字节、一个字符 3 字节,段账与 12 个 ASCII 逐值相同(dataBits 96、rawBits 108),没有 13 bit/字的码流。

23 字节的同一条内容在四个等级下的落点 版本 模块数 cap pad ec L v2 25x25 32 7 10 M v2 25x25 26 1 16 Q v3 29x29 32 7 36 H v4 33x33 34 9 64 模块数 = 17 + 4x 版本

图 1 同一条内容在四个等级下的段账

容量阶梯与那个不够用的 +2

版本选择只有一句判据:第 236–238 行拿容量表比对 byteLen + 2,1 到 10 逐个试,全不中返回 -1。每档右端点恰好是表值减 2:可用上限 L 269 / M 211 / Q 149 / H 117 字节,270 与 118 起就返回 null。预留的 2 字节按 12 bit 头算,v10 是 20 bit:117 字节在 H 级选中 v10(表值 119),实测 dataBytes 120 大于 cap 119、padBytes −1。

模块数是死公式 size = 17 + 4 × version,80 个「等级 × 输入」格子无一例外:尺寸变的是版本,不是内容;v10 的容量从 271 降到 119,H 的实际可用只有 L 的 43.5%。

纠错码字:个数对得上,字节对不上

ec 值直接进 rsEncode 的 eccLen,12 组实测输出长度全命中;数据码字与纠错码字永远拼成 1 个块(第 317–318 行 concat,interleave 命中 0 次),v10 总码字 344。

长度对,内容不对:与标准生成多项式(既约多项式 0x11d)对照,eccLen 224 时 224/224 全不同;data 拼 ecc 求伴随式,L 级 v1 是 7/7、H 级 v1 是 17/17、L 级 v10 是 72/72,全非零。反解出的生成多项式除 n 等于 1 外,每个恰好错 n − 1 个系数。

固定掩码把时序行抹成一片白

实现里只有一条掩码:第 422 行 function mask(r, c) { return (r + c) % 2 === 0; },maskPattern、penalty、score 命中都是 0 次,不存在 8 种掩码打分选优的分支。

v10 的 57x57 里,这两处被同一枚棋盘异或改掉 这一页 0/40 0/16 0/12 0/4 全白 时序第 6 行黑格实测(上排版本:10、4、3、1) 5x5 对齐块黑格:标准 17,实测 12 12

图 2 时序第 6 行与 5×5 对齐块改动后的黑格数

代价落在功能图案上:第 426–428 行的 isFunction 只排除三个定位角邻域,而时序值本就是 (i % 2 === 0) ? 1 : 0,与这枚棋盘同相,异或后恒 0:v10 的 40 个时序位黑格 0/40。5×5 对齐块应是环加中心 17 个黑格,实测只剩 12 个,25 个模块里 13 个与标准图案不符。

格式信息:值算对,位置写错

第 437–445 行 eccLevelBits 是 {L:1, M:0, Q:3, H:2},BCH 生成式 0x537、异或 0x5412,H 级得 0x1689,第 438 行掩码位硬编码 0。30 条写入只落在 21 个模块上、9 个被写两次,标准左下副本该占的 7 格从不出现在写入目标里。

文件名里的 33 是模块数

第 778 行 qrSize = result.size 是模块数,滑杆 128–512 才是像素期望值;静默区开时 totalSize 是模块数加 8,pixelSize = Math.floor(displaySize / totalSize)。33 模块这一档,13 档滑杆只产出 11 种像素,公式复算 13 组全等、偏差只小不大,最大 38 px。

三个档位的文件名与 IHDR
滑杆标称下载文件名IHDR 实际字节
128 pxqrcode_33x33.png123×1232059
320 pxqrcode_33x33.png287×2874478
512 pxqrcode_33x33.png492×4928547

第 800 行的文件名也取 qrSize,三档同名 qrcode_33x33.png;SVG 侧名同 33x33、根元素 width="410"。开头那对反差就是这么来的:模块越少、totalSize 越小,floor 之后画布反而更大。

报错态与导出字节

超容量时页面不清状态:第 770–776 行只改显示、不置空 qrMatrix,第 794 行 if (!qrMatrix) 照样放行——报错态下点下载仍拿到 4478 字节、cksum 511620 的上一张码;只有第 844 行的清空能把它置 null,此后下载给「请先生成二维码」。

导出字节都是 PNG 口径:第 796 行 canvas.toBlob 不带第二个 mime 参数,实测魔数 89504e470d0a1a0a。码点方块 4478、圆角 7558、圆点 9466;渐变打开 47542,是默认值的 10.6 倍。SVG 只认版本与配色,恒 32316 字节 / 532 个 rect;两遍运行 11 个可比场景的 bytes 与 cksum 配对逐值相同。

四件事与几段追问

模式指示器恒 0100、阶梯右端点恒等于表值减 2、纠错码字只保证个数、掩码是一枚固定棋盘,这就是这一页的编码器走到的位置。

Q:为什么等级越高,码就越大?

同一版本的可用字节被高等级吃掉:23 字节在 L/M 还是 v2,到 Q 要 v3、到 H 要 v4,模块数按 size = 17 + 4 × version 涨上去。

Q:这些实测能说明码能不能被扫出来吗?

不能,本篇没跑任何解码器;「H (30%)」只是 option 文本、Logo 滑杆上限是 35,都不在本篇结论里。解码侧与每模块像素的生死线,另一篇二维码解码实测记的是另一面。

Q:报错以后为什么还能下载出一张码?

失败分支只改显示、不清状态,导出用的是上一次成功的矩阵;唯一能把矩阵清零的入口是清空。

📎 相关文章

帧间隔填 137 ms,成品每帧其实停 140 ms:tick 量化、4096 页填充与跟着第一张走的画布 2026-10-30 拼豆图纸的色号是怎么挑出来的:开一次抖动,1024 格里有 150 格被改写 2026-10-30 横图裁掉 25%、竖图裁掉 43.75%:7 个拼图模板逐格算一笔几何账 2026-10-28 浅色底上的白字水印设成 5% 等于没加:导出 4782 字节与底图一字节不差 2026-10-28 1 月 31 日加一个月落在哪天?这页只给天数,28 天与 29 天差 86400 秒 2026-10-28