Skip to content

How the blank space works

Emptying a QR code is not about deleting data. It is about finding the parts that never carried meaning, and computing values that make those parts render white.

Half of a QR code is padding

A QR code is a fixed-size container. A version 5 code holds 134 codewords, 108 of which are data. A URL like HTTPS://EXAMPLE.COM needs about 14. What happens to the other 94?

The spec says to fill them with 0xEC and 0x11 alternating. Those bytes carry no meaning, but 0xEC has five of its eight bits set, so on screen it becomes a fairly dense block of black. That is a large part of why QR codes look so busy.

Decoders never read the padding

The data region starts with a mode indicator and a length, and when the real content ends a terminator of four zero bits follows. ZXing, jsQR and most other decoders stop reading the moment they hit it. Whatever comes after makes no difference to the result.

Cancelling the mask

The final encoding step picks one of eight lattice patterns and XORs it across the whole data region. Which pattern was used is recorded separately near the top-left corner, so the reader XORs the same pattern back to recover the original bits.

That is the opening. XOR of two equal values is zero. Put the mask value for that exact position into the padding bit, and after masking the module comes out as zero, which is white.

Three lines of work. Precompute the coordinate each bit lands on. Evaluate the mask function at that coordinate. Store that value in the padding. The XOR then cancels.

What stays black

  • The payload. The URL itself. Shorter URLs take fewer modules.
  • Error correction codewords. Reed-Solomon computes them, so you do not get to choose them, and statistically half their bits are set. They are roughly 20 percent of all codewords.

So at error level L the floor sits around 11 percent black. An ordinary QR code is about 47 percent, which makes this roughly a quarter as dense.

You can push the error bytes a little too

The error correction bytes are a function of the padding. Change the last two padding bytes and the entire block is recomputed, landing on a slightly different number of black modules each time. Repeat a few hundred times, keep the whitest result, and you shave off another percent or two.

Measured results

Share of black modules by version and error level
LevelV2V3V5V7V10
L27%20%15%13%12%
M34%28%21%21%21%
Q38%37%32%31%29%
Hn/a42%40%36%34%

Larger versions get emptier, but the returns flatten around V7 because the error correction share stays roughly constant no matter the version.

It is not free

A large white area gives the binarizer nothing to calibrate against. The dense texture of a normal QR code acts as its own reference; empty that out and the reference disappears. In dim light or a shaky photo, specks appear where there were none, and if the error correction bytes cannot absorb them the read fails. See choosing an error level and making it scan reliably for how to trade this off.

Does this break the spec?

Only the contents of the padding differ. The mask number is recorded exactly as required and Reed-Solomon is computed normally. In strict spec mode this encoder matches the reference `qrcode` library bit for bit across versions 1 to 10 at all four error levels.