等级只买到「数据区」的耐受力
二维码解码看起来只有「认出来」和「认不出来」两态,但决定结果的变量比想象中多:纠错等级、污损落在哪个区域、每个模块摊到几个像素。这一篇在同一条解码路径上把边界量化了一遍。
v1 是 21×21=441 个模块,去掉定位、定时、格式信息后能翻的数据格池有 208 个。只在数据区逐格翻,解不开的起点按等级排开:L 最后成功 3–4 格、5 格失败;M 5 格、6 格失败;Q 6 格、7 格失败;H 撑到 9–12 格、10–13 格失败。等级确实在买耐受度。
但同一份码图换一种坏法,结论会翻过来。把两条时序图案一共 10 格全部翻转,L/M/Q/H 四个等级反而都还解得开:时序格本身不携带数据,破坏它并不等于吃掉等量的纠错容量。这套判定都跑在二维码解码里。
位置比等级更重要
中心白色 logo 能盖多大,和等级同向:L 边长 3,占 441 模块的 2 %;M 与 Q 边长 6,36/441≈8.2 %;H 边长 8,64/441≈14.5 %。码越大越经盖:29×29=841 模块的 v3 码能盖到边长 8(7.6 %),37×37=1369 模块的码能盖到 11(8.8 %)。
可这块白一压到左上定位角,等级就失效:H 边长 1 即失败,L 反而撑到 3。把定位角区 147 格逐格翻黑,H/Q 常在 1–3 格就崩,L/M 却能撑到 2–7 格。同一枚码、同样的破坏当量,只换区域,排序整个反过来。
| 被破坏的区域 | L | M | Q | H |
|---|---|---|---|---|
| 数据区(池 208 格) | 3–4 | 5 | 6 | 9–12 |
| 定位角区(147 格) | 2–7 | 2–5 | 0–2 | 0–3 |
| 时序区(10 格) | 10 格全翻仍解得开 | |||
| 格式信息区(33 格) | 7–11 | 8–9 | 6–9 | 6–11 |
生死线是每模块像素数
把同一枚 v1 码按不同静默区导出一串尺寸,失败点不在图片够不够大,而在一个模块摊到几个像素。每模块 1 px 的三种尺寸一律失败:静默 4 的 29×29、静默 2 的 25×25、静默 0 的 21×21。每模块 2 px 则一律成功,连零静默区的 42×42 都解得开;580 px 缩到 58 px 仍是 2 px/模块,载荷照样逐字精确。
往大走也没有第二个坑:模板里 maxDim=1000 只有一条等比降采样,1015×1015、1160×1160、2900×2900 全被压成 1000×1000,摊下来每模块 34.483 px,非整数仍然逐字解出;740×740 与 888×888 不触顶,按原尺寸进像素。预览的 max-height:300px 只管显示,canvas 取的是自然尺寸,900×900 的大图不会因为预览变小而丢像素。
反色只来自一个参数
jsQR 只被调用一次:没有超时、没有多尺度重试、没有二值化预处理,返回 null 就直奔失败面板,能不能救回来全看传进去的那个选项。inversionAttempts:'attemptBoth' 是反色能力的唯一来源:4 枚黑底白码(L/M/Q/H)4/4 解出,同一批码换成 dontInvert 再喂一次 0/4,连 #111111 深底上的反色码也照样解出。
代码里确实有一条原生 BarcodeDetector 分支,空结果与异常两条降级出口都在。但本机 headless Chrome 四种跑法里特性检测恒为 false,全程走 jsQR:23 次成功解码(15 个标准 fixture 加 8 个真实载荷)的「识别引擎」全是 jsQR。原生那条本机进不去,属于测不出来的东西,这篇不给任何两引擎对比数字。
格式同理。页面列了 PNG / JPG / WebP / GIF / BMP 五种,代码侧只按 image/* 匹配;而 15 个 fixture 全是 PNG,GIF 连样本都没造出来,这行只是文字声明。
「用解码验证生成」闭环不成立
拿站内二维码生成器产出的码回到这条 jsQR 路径上解,20 组(5 种载荷 × L/M/Q/H,scale 10 与 scale 4)全部 null。问题不在解码侧,根因写在生成的矩阵里:掩码函数 (r+c)%2===0 本该只作用数据区,可 isFunction 只标了三个定位角,时序、对齐、格式信息格于是被一并亦或。v1 码第 6 横排的时序读成 10000,标准是 10101;233 个功能格里错 24–34 个(L 32、M 34、Q 24),对照用的 15 个 fixture 则全部解出。
生成侧的容量表也有算术差:v1 到 v9 的 36 个等级组合比标准多报 2 字节,v10 的 4 个组合少报 3 字节。版本选择写的是 cap >= byteLen + 2,白留 2 字节,于是 15 字节停在 v1、16 字节就跳 v2。最大收得下的载荷 L 269、M 211、Q 149、H 117,首个被拒 270、212、150、118。
批不了量,还有两个缺陷
文件选择框没有 multiple(实测 .multiple 为 false),拖拽也只取 files[0],第二个及以后静默丢弃。两个码并排画进一张 600×300 的图,渲染只被调用 1 次,返回右侧那个码。全文件也没有任何 file.size 判断,「图片不能超过多大」这类说法同样无据。
两个实测缺陷。第一个:「重新上传」清了面板却没清结果数据,于是「解出内容→重新上传→传一张失败的图→点复制」时,屏幕写着失败文案,复制到的却是上一张内容,toast 还是空的。第二个:解码库挂在 CDN 上,取不到时抛 Uncaught TypeError,面板 innerHTML 长度 0——既不是成功文案的 372,也不是失败文案的 194,只剩一片空白。
顺手答疑
Q:为什么我自己生成的二维码解不开?
先别怀疑等级。站内生成的码在这条路径上 20 组全 null,根子是掩码被亦或到了功能格上。排查按实测的性价比走:先看每模块有没有 2 px,再看定位角和时序区有没有被盖,最后核对载荷是否越过 269 / 211 / 149 / 117。
Q:图片是不是越大越容易解出来?
不是,下限在像素/模块而不是尺寸:1 px/模块 的三种尺寸全失败,2 px/模块 的 42×42 零静默也成功;再大只是被等比压到 1000×1000,非整数的 34.483 px/模块照样逐字解出。
Q:深色底的反色码能解吗?一次能传几张?
反色能,#111111 深底白码解得开,但完全依赖 attemptBoth 这一个参数,不是引擎自带。数量是硬限制:一次一个文件、一图一个结果,只能一张张走同一条路径。