The 30% logo rule is wrong in both directions
Every QR generator repeats the same advice about how big a logo can be. Error correction is applied per Reed-Solomon block, and a single finder module is fatal at any level — so the percentage is measuring the wrong thing entirely.

the part they actually sell you
Open any QR code generator and look at what costs money. It is never the encoder. It is the colours, the shapes, the logo, the SVG export. On qr-code-generator.com the free tier gives you a JPG and nothing else — PNG, SVG, colour and logos all sit behind an account. On the-qrcode-generator.com the codes are free and removing the watermark is not.
That split is not an accident. QR encoding is ISO/IEC 18004, published, and Denso Wave chose not to enforce its patents. There is no licence to buy and no algorithm to protect. Generating the pattern is a commodity, so the business has to be the layer on top: the part where you make the code yours.
Which is awkward, because that layer is exactly where QR codes break. And the tools selling it to you almost never say when you have broken one.
I went looking for the real numbers behind the advice everyone repeats. Most of it turns out to be wrong.
the rule everyone repeats
You can cover up to 30% of a QR code with a logo at error correction level H.
You will find this on nearly every design blog. It is wrong in both directions, and understanding why changes how you place a logo.
Error correction is applied per block, not across the code. Reed-Solomon does not protect a QR code as one unit. The codewords are split into blocks — a version 5-Q code has four of them — and each block carries its own parity and its own correction budget. Interleaving then spreads those blocks across the grid so that localised damage lands one codeword deep in each, rather than wiping one out entirely.
The consequence is that where the damage falls matters more than how much of it there is. Damage concentrated in a single block can destroy a code covering well under 30%. The same amount spread evenly survives comfortably. A percentage cannot distinguish those two cases, so it cannot answer the question.
A finder pattern is fatal at any level. The three large squares in the corners are how a scanner locates and orients the code. That happens before error correction runs at all. Cover one module of one finder and no amount of redundancy saves you — level H does not help, because the decoder never gets far enough to use it.
So the 30% figure is measuring the wrong thing, and it is measuring it with a number that does not correspond to the failure mode.
what an exact answer looks like
To compute the real number you need three things that a rendered bitmap does not contain:
- which modules are function patterns rather than data
- the order the data bits were written into the grid
- which Reed-Solomon block each codeword belongs to
That is the reason I wrote the encoder rather than importing one. A library hands you a boolean grid. Owning the encoder means every module can carry its structural role, and the placement walk and interleave map can be reused in reverse.
The calculation then falls out:
// covered modules -> bit indices -> codewords -> owning block
const damaged = new Set<number>();
for (let bit = 0; bit < sequence.length; bit++) {
if (covered.has(sequence[bit])) damaged.add(bit >>> 3);
}
const perBlock = new Int32Array(layout.numBlocks);
for (const codeword of damaged) perBlock[layout.ownerOfCodeword[codeword]]++;
// Reed-Solomon corrects half as many errors as it has check symbols
// when the positions are unknown, which is the case for a logo.
const worst = Math.max(...perBlock);
const recoverable = !breaksFinder && worst <= layout.correctablePerBlock;The number that matters is worst / correctablePerBlock — how much of the worst block's budget the logo consumes. Not a percentage of area. In the tool this shows up as "error budget used", and the advice is to keep it under 70% so there is headroom left for a scratch, a fold, or a bad print run.
but does the prediction hold?
A model that is elegant and wrong is worse than a rule of thumb, so I tested it against reality. For each of 192 combinations — three payloads, four error correction levels, eight logo sizes, two mask shapes — the covered modules were actually cleared and the damaged code handed to an independent decoder.
- 114 predicted recoverable, and decoded
- 72 predicted unrecoverable, and failed
- 0 predicted recoverable but failed
- 6 predicted unrecoverable but decoded anyway
Zero dangerous false positives. It is wrong six times, always in the safe direction — it calls a code unrecoverable when error correction would in fact have scraped through.
That asymmetry is the whole point. Being slightly pessimistic costs a user a marginally smaller logo. Being optimistic costs them five thousand printed flyers.
the shape nobody warns you about
Here is a failure I did not expect to find, and it explains a complaint you may have heard: "the QR works on my phone but not on the scanner at the shop."
Round eye centres are everywhere in QR design. They look good. They also fail on a whole family of scanners, for a reason that is entirely mechanical.
ZXing-derived decoders verify a finder pattern by checking its 1:1:3:1:1 dark-light ratio — horizontally, vertically, and diagonally. Through the centre line, a circle and a square are indistinguishable. Along the diagonal they are not. A 3×3 square core measures 4.24 modules corner to corner. A circle inscribed in it measures 3.0. The diagonal check fails, and the decoder never sees a finder pattern at all.
Newer phone cameras use machine-learning detection and tolerate it, which is precisely why circular eyes look fine when you test on your own handset and fail on cheaper hardware.
I measured every shape rather than guessing. Each was rendered, rasterised and decoded across three payloads, two error correction levels and six scales — 36 attempts per shape:
eye centre decoded
square 36/36 100%
rounded 34/36 94%
dot (circle) 19/36 53%
leaf 19/36 53%
diamond 0/36 0%The diamond result was not a trade-off, it was a defect — an inscribed diamond holds half the area of the square and destroys the ratio outright. I rebuilt it as a superellipse, which recovered some of it, and it is still labelled the worst option in the interface.
Two caveats worth stating plainly. This decoder is one conservative data point, not ground truth — I found a bug in it during this work, which I will come back to. And my rasteriser is hard-edged with no antialiasing, which is harsher than a real camera. The ranking is the useful part, not the absolute percentages.
Rather than hide the shapes or ship them silently, the tool labels each one with the margin it costs.
why printed codes fail
The rule of thumb here is that a code reads from roughly ten times its own width, so a code scanned from 30cm should be about 3cm across. That is correct and insufficient.
The second constraint is module pitch — the size of one individual square. Below about 0.4mm, ink spread closes the gaps between modules whatever the overall dimensions say. A dense code sized correctly for its scan distance can still have modules under that floor.
This is the failure I suspect is most common in print, because everything looks right on screen. A version 25 code at 3cm has modules around 0.25mm. It satisfies the distance rule and it will not read. The fix is either a bigger code or less content, and content is usually the easier lever: shortening a URL can drop several versions.
testing an encoder you wrote yourself
There is a circularity problem in this kind of project. If I write the encoder and I write the test, then any misunderstanding I have about the specification lives in both, and the suite passes while the codes fail.
The only way out is verification that shares no assumptions with the implementation. So every generated matrix is decoded by an independent decoder, and all 160 version-and-level combinations are exercised on every run. That suite is not decoration. It caught real defects:
- Three errors in the error-correction block tables. Version 8 at level H had the wrong block count, and everything from version 32 upward was shifted by one. Eleven combinations produced codes that looked perfectly normal and did not scan.
- Four eye-frame shapes deleting finder modules. Corner radii large enough to round away the corner modules of the ring — restyling the pattern into a broken one. The limit turns out to be a radius of about 1.7 modules, past which the arc stops covering the module's own centre.
And once, memorably, the decoder was wrong rather than my code. Version 23 at level L refused to decode at every mask, payload and scale, while M, Q and H were fine. The cause was in the decoder's table: its alignment-pattern centres for version 23 are [6, 30, 54, 74, 102], where the specification's spacing rule gives 78, not 74.
I did not take that on faith. Version 23 is the only irregularly spaced entry in its entire table; patching the value made all four levels decode; and only level L failing is exactly what you would expect, since the higher levels have enough redundancy to absorb a handful of misread modules. The library has not shipped a release in about five years.
The lesson I took from it is not that the library is bad. It is that "my tests fail" and "my code is wrong" are different statements, and the gap between them is worth investigating before you start changing things.
what shipped
QR Studio is live. It has 28 content types, ten module shapes, gradients, logos, and export to SVG, PNG, JPG, WebP, PDF and EPS with no watermark and no account. The design serialises into the URL, so a link carries the whole thing and there is nothing to store.
Everything runs in the browser. Nothing is uploaded, which matters more than it sounds — people encode WiFi passwords, vCards and crypto addresses into these, and every hosted generator sends that payload to a server to render it. You can verify the claim by opening the network tab, or by disconnecting entirely and generating a code anyway.
There are no runtime dependencies. The encoder is about 800 lines implementing the specification directly, and writing it rather than importing it is what made the validation possible at all.
The cover image on this post is a QR code the tool generated. It points at the tool. It is also verified in the build that produced it — the composited pixels are read back as greyscale, the way a camera would, and checked against what they should say.
That is the whole argument, really. The interesting question was never whether a generator can draw a pretty code. It is whether anyone bothered to check that the pretty code still works.