雜記 › 技術筆記 › 二維碼解碼實測:糾錯等級救不了遮擋,每模組 2 像素才是生死線

二維碼解碼實測:糾錯等級救不了遮擋,每模組 2 像素才是生死線

等級只買到「資料區」的耐受力

二維碼解碼看起來只有「認出來」和「認不出來」兩態,但決定結果的變數比想像中多:糾錯等級、污損落在哪個區域、每個模組攤到幾個像素。這一篇在同一條解碼路徑上把邊界量化了一遍。

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 格。同一枚碼、同樣的破壞當量,只換區域,排序整個反過來。

同一枚 v1 碼分區域翻格:最後還能解開的格數
被破壞的區域LMQH
資料區(池 208 格)3–4569–12
定位角區(147 格)2–72–50–20–3
時序區(10 格)10 格全翻仍解得開
格式資訊區(33 格)7–118–96–96–11
圖 1 換個區域,排序就反過來 4 7 5 5 6 2 12 3 L M Q H 資料區 208 格 定位角區 147 格

生死線是每模組像素數

把同一枚 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 的大圖不會因為預覽變小而丟像素。

圖 2 每模組像素數決定成敗 1 px 29×29 25×25 21×21 全失敗 2 px 42×42 零靜默仍成功 34.483 px 2900×2900 壓到 1000×1000

反色只來自一個參數

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 這一個參數,不是引擎自帶。數量是硬限制:一次一個檔案、一圖一個結果,只能一張張走同一條路徑。

📎 相關文章

文本合併後行數對不上:缺尾末換行、被吃掉的 BOM 和不可靠的順序 2026-10-26 Excel 貼上轉 JSON:日期、長數字與精度在貼上那一刻就已經變了 2026-10-26 條碼解碼只吐原文:EAN-13 檢查碼、UPC-A 的前導零與 CODE128 的模 103 2026-10-26 字頻還是詞頻:同一句中文,一份 23 項榜單和一份空表 2026-10-26 CSS 壓縮沒吃掉 calc 的空格,美化卻把 .btn:hover 劈成了兩截 2026-10-26