Error correction levels, and what they really buy you
Every QR code carries redundant data so it can be read when part of it is unreadable. You choose how much. The four levels are widely quoted as "7%, 15%, 25% and 30% damage tolerance", and that shorthand is misleading in a way that matters when you are placing a logo.
The four levels
| Level | Name | Approximate recovery | Typical use |
|---|---|---|---|
| L | Low | About 7% | Clean surface, size at a premium, large payload |
| M | Medium | About 15% | The sensible default for print |
| Q | Quartile | About 25% | Small logo, or a surface that gets handled |
| H | High | About 30% | Large logo, industrial or outdoor use |
The mechanism is Reed-Solomon coding, the same family used on compact discs and in deep-space telemetry. The encoder appends parity codewords to your data codewords. A decoder that finds some codewords corrupted can reconstruct them from the parity, up to a limit set by how much parity there is.
The percentage is of codewords, not of the picture
This is the part the shorthand gets wrong. The figures describe the proportion of codewords that can be restored, and a codeword is eight modules of data. They do not describe the proportion of the printed square you may obliterate.
Two things pull in opposite directions here. Working against you, the scaffolding is not protected: the finder patterns, timing strips and format bits are not part of the error-corrected payload, so covering a finder pattern does not consume 7% of your budget, it stops the code being found at all. Working for you, data and parity codewords are interleaved across the symbol rather than stored in separate blocks, so one contiguous blot is spread across many blocks and each one sees only a little damage. That is exactly the shape Reed-Solomon handles best.
What you pay for it
Parity occupies space that would otherwise hold your content. At a fixed version, raising the level lowers capacity sharply.
| Version | Grid | L | M | Q | H |
|---|---|---|---|---|---|
| 2 | 25 x 25 | 32 | 26 | 20 | 14 |
| 3 | 29 x 29 | 53 | 42 | 32 | 24 |
| 4 | 33 x 33 | 78 | 62 | 46 | 34 |
| 5 | 37 x 37 | 106 | 84 | 60 | 44 |
Read across any row and the pattern is stark: moving from L to H costs you more than half your capacity. Read down a column and you can see the encoder's escape route. It will not refuse your content for want of room; it will step up to a bigger version. Which leads to the trap.
The logo trap
The standard advice for putting a logo in the middle of a code is to raise the error correction level. That advice is right as far as it goes, and it quietly assumes the code is getting bigger on the page. Often it is not.
Suppose you have a link that fits version 3 at level M, and you print it 25mm wide. That is 29 modules across 25mm, so each module is about 0.86mm. Now you add a logo and switch to level H. The content no longer fits version 3, so the encoder steps up to a larger grid. The printed square is still 25mm, so the same width is now divided among more modules and each one has shrunk.
You have bought redundancy and paid for it in module size, and module size is what decides whether a camera at arm's length can resolve the pattern at all. On a clean flat surface that trade is usually still worth it. On a code that will be printed small, read at distance, or produced on a press that spreads ink, it can leave you worse off than the plain version 3 at level M you started with.
Choosing, in practice
- Start at M. It is the default in most encoders for good reason and handles ordinary print, handling and lighting.
- Go to Q or H if a logo covers the center, if the surface will be scuffed, wet, curved or outdoors, or if the code is going somewhere it cannot be reprinted.
- Go to L only when the payload is large and the surface is clean and flat, and you are trying to keep the module count down. A long URL at level L on a laser-printed sheet is a reasonable use.
- Whatever you choose, test the physical artifact rather than the screen. Error correction compensates for damage, not for a module size the camera cannot resolve.