One character unchanged, the grid grows from 25 cells to 33
Take one 23-byte string and move the error-correction level from L up to H: the statistics line changes from 25x25 to 33x33, while the exported canvas shrinks from 297x297 to 287x287. The input was never edited, yet the modules get denser and the picture gets smaller. The two readings come from different stages of one run.
The encoder on this page was written by hand: lines 158–465 of the QR Code Generator template carry a bit stream running from a GF(256) lookup table to matrix generation, with no external library referenced.
One bit stream, mode indicator fixed at 0100
Line 284 is the only mode-indicator write in the whole file, bits.push(0, 1, 0, 0), and it sits outside every conditional branch, so the mode is permanently 0100 and each character costs a flat 8 bits. Digit-only input, alphanumeric input, Chinese characters and emoji all come back cut into exactly 1 segment, and the keywords numeric, alphanumeric and KANJI are never hit, so there is no second code path. The length field is 8 bits, or 16 bits once v10 is reached, which makes the header 12 or 20 bits. Chinese characters get no segment of their own: the sample 中文测试 is 4 characters, 12 bytes, 3 bytes per character, and its segment arithmetic matches 12 ASCII characters value for value (dataBits 96, rawBits 108). No 13 bit-per-character stream exists.
Figure 1 Segment arithmetic for one input at four levels
The capacity ladder and the +2 that is too small
Version selection is one sentence: lines 236–238 compare the capacity table against byteLen + 2, try versions 1 to 10 in order, and return -1 if none fits. Since the byte count is always inflated by that reserve, the right endpoint of every step is exactly the table value minus 2. The usable ceilings are L 269 / M 211 / Q 149 / H 117 bytes, and at 270 bytes for L, or 118 for H, the page already returns null. The reserve assumes a 12 bit header, but v10 is where the header becomes 20 bits, so the shortcut stops being safe there: 117 bytes at level H select v10, table value 119, and the measurement gives dataBytes 120 against cap 119 with padBytes at −1. A negative padding count is what a borrowed rule of thumb leaves behind.
The module count is a hard formula, size = 17 + 4 × version, and all 80 level-by-input cells follow it without exception: what changes the size is the version, never the content. Capacity at v10 falls from 271 to 119, so level H leaves only 43.5% of the payload level L offers.
Error-correction codewords: the count matches, the bytes do not
Each ec value goes straight into eccLen inside rsEncode, and all 12 measured output lengths hit, so a count-only check finds nothing. Data and error-correction codewords are always concatenated into 1 block by lines 317–318, interleave is never hit, and v10 totals 344 codewords.
The length is right and the content is not. Beside the standard generator polynomial, built from the reducible polynomial 0x11d, the comparison at eccLen 224 finds 224 differences out of 224 bytes. Joining data and ecc and taking the syndromes gives 7/7 non-zero at level L v1, 17/17 at level H v1 and 72/72 at level L v10, where a valid codeword gives zeros. Recovering the polynomial actually used shows why: exactly n − 1 of its n coefficients disagree with the standard one.
A fixed mask wipes the timing row white
There is one mask in the implementation, and line 422 is the whole selection logic: function mask(r, c) { return (r + c) % 2 === 0; }. The identifiers maskPattern, penalty and score are never hit, so the branch that would score 8 candidate masks and keep the best is absent; the checkerboard hits every module not protected.
Figure 2 Dark cells in timing row 6 and the 5x5 alignment block after the flip
The cost falls on the function patterns. The isFunction guard on lines 426–428 excludes only the neighbourhoods of the three finder patterns, so timing rows, alignment blocks and format strips get masked like ordinary data. A timing pattern alternates on its own index, so its value is already (i % 2 === 0) ? 1 : 0, exactly in phase with this checkerboard, and the XOR drives it to 0 instead of scrambling it: of the 40 timing positions in v10, 0/40 stay dark. The 5x5 alignment block should be a ring plus a centre, 17 dark modules, but only 12 remain, and 13 of its 25 modules disagree with the standard shape.
Format information: right value, wrong positions
The eccLevelBits table on lines 437–445 reads {L:1, M:0, Q:3, H:2}, the BCH remainder uses generator 0x537 and XOR constant 0x5412, so level H produces 0x1689, while the mask bit is hard-coded to 0 on line 438. The write targets show the placement problem: the 30 format bits land on only 21 modules, 9 of them written twice so the last write wins, and the 7 cells reserved for the standard bottom-left copy never appear among the targets.
The 33 in the file name is modules, not pixels
On line 778 qrSize = result.size is the module count, while only the 128–512 slider range holds a pixel expectation. With the quiet zone on, totalSize is the module count plus 8 and the canvas edge comes out of pixelSize = Math.floor(displaySize / totalSize), a rounded product rather than the number asked for. At the 33-module step the 13 slider positions collapse into 11 distinct canvas sizes, and recomputing the formula reproduces all 13 measurements exactly; the shortfall only goes one way, up to 38 px.
| Slider label | Downloaded file name | Actual IHDR edge | Bytes |
|---|---|---|---|
| 128 px | qrcode_33x33.png | 123x123 | 2059 |
| 320 px | qrcode_33x33.png | 287x287 | 4478 |
| 512 px | qrcode_33x33.png | 492x492 | 8547 |
The name built on line 800 also reads qrSize, so all three positions download a file called qrcode_33x33.png even though the images differ; the SVG side names itself with the same 33x33 while its root element declares width="410". The IHDR header is written from the finished bitmap, not from the matrix. That is also where the opening contrast comes from: fewer modules make totalSize smaller, floor keeps more, and the canvas grows while the level goes up.
The error state and the exported bytes
Going over capacity does not reset the page. Lines 770–776 change only what is displayed and never null out qrMatrix, so the guard on line 794, if (!qrMatrix), still lets the export through: while the error message is on screen, download delivers the previous code again, the same 4478 bytes with cksum 511620. Only the clear action on line 844 writes null into the matrix, after which download answers with the prompt 请先生成二维码.
Every export is a PNG. The canvas.toBlob call on line 796 passes no second mime argument, and the measured magic number is 89504e470d0a1a0a. The three module shapes differ only in edge detail: squares 4478 bytes, rounded corners 7558, dots 9466. Turning the gradient on jumps to 47542, which is 10.6 times the default, since a gradient destroys the run-length structure PNG relies on. The SVG path reads only version and colours, so it stays at 32316 bytes and 532 rect elements whatever is selected. Repeating the sweep paired the bytes and the cksum of the 11 comparable scenarios value for value.
Four findings and three follow-ups
The mode indicator is fixed at 0100, the right end of every capacity step equals the table value minus 2, the error-correction codewords guarantee a count and nothing else, and the mask is one permanent checkerboard. That is how far the encoder on this page has been taken.
Q: Why does a higher level produce a bigger code?
Because a higher level spends the budget of one version on redundancy. The same 23 bytes still fit v2 at L and M, need v3 at Q and v4 at H, and the module count grows along size = 17 + 4 × version though the string never changed.
Q: Do these measurements say whether the code can be scanned?
They do not, and no decoder was run for this article, so the findings stop at pattern level: codeword content, timing and alignment shapes, format placement. The text "H (30%)" is only option text and the Logo slider tops out at 35, neither of which belongs in these conclusions. The decoding side, and the pixels-per-module line between a working read and a failed one, are recorded from the other direction in QR Code Decoding Tested.
Q: Why is a code still downloadable after an error?
Because the failure branch only redraws the readout and never clears the stored matrix, so the export keeps using the last successful run. The clear action is the only entry point writing null into it.