同一個字沒改,格子從 25 個變成 33 個
同一段 23 位元組的內容,糾錯等級從 L 撥到 H,統計那行從「尺寸:25x25」變成「尺寸:33x33」,匯出的畫布卻從 297×297 縮到 287×287。一個字沒改,格子變密、畫布反而變小,這兩件事各自從哪來?
這一頁的編碼器是自己寫的:QR Code 產生器 範本第 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/字的碼流。
圖 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 種遮罩打分選優的分支。
圖 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 實際 | 位元組 |
|---|---|---|---|
| 128 px | qrcode_33x33.png | 123×123 | 2059 |
| 320 px | qrcode_33x33.png | 287×287 | 4478 |
| 512 px | qrcode_33x33.png | 492×492 | 8547 |
第 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:報錯以後為什麼還能下載出一張碼?
失敗分支只改顯示、不清狀態,匯出用的是上一次成功的矩陣;唯一能把矩陣清零的入口是清空。