SWAPQR← Guide

Why an artistic QR code can fail when you scan it too closely

Everyone expects a QR code to fail because it is too small, too far, too dim. Photo QR codes have the opposite failure too: they can read perfectly from a distance and fail when you bring the phone right up to them. Here is why, with the numbers from the measurement that found it.

Make a QR code in the browser →Print the square once. Decide later where it goes.

How a scanner decides what is dark

Before a reader can look for modules, it has to turn a photograph into black and white. A global threshold does not survive real lighting — a shadow across one corner would black out half the code — so most decoders threshold locally. ZXing, the open-source library a great deal of barcode scanning is still descended from, divides the image into 8×8 pixel blocks, computes each block's average, smooths it against neighbouring blocks, and uses that as the threshold for those 64 pixels. A pixel darker than its block's threshold is dark; the rest are light. For a normal QR code this is close to free. Whatever the block size, a module is solid, so the average of a block is either near-dark or near-light, or sits between the two when the block straddles a boundary, and the decision is easy either way. The scheme is robust, cheap and has been copied widely, which is exactly why it matters: the decoder your printed code meets in the field may well be one that thresholds this way.

What changes when the modules are made of dither

In a dithered code, a module is not solid. It is a small forced core surrounded by pixels that carry the photograph. Its average is correct; its texture is not uniform. Now consider scan distance in the only unit that matters: camera pixels per module. Hold the phone far enough away that a module lands on 5–8 pixels, and an 8×8 block straddles module boundaries. The local average is roughly "this module and its neighbours", the threshold sits between dark and light, and the code reads. Bring the phone closer so a module covers 20 pixels, and an 8×8 block now fits entirely inside a single module. The block's average is that module's own local texture. The threshold follows the dither instead of the module — and the decoder recovers the speckle, not the code. Nothing about the print has changed; only the number of sensor pixels each module covers, and with it the relationship between the decoder's block and the module. That is why the failure is a property of distance rather than of size or lighting.

The number

We measured it while building SWAPQR's photo mode: three payload lengths, six image types, both colour polarities, three balance settings, seven simulated scan distances, decoded by two independent libraries. The boundary is clean. With about 20% of each module forced — the photo-heavy setting — and with about 44% forced, the codes read at 5–8 pixels per module and ZXing-class readers became unreliable in close-up, at 10–28 pixels per module. At about 60% forced and above, they read at every distance. Once more than roughly half of a module's area is forced solid, any 8×8 block that falls inside the module lands mostly on solid pixels, and the effect disappears. Below that, it does not — and no amount of tuning the tone bias fixes it, because a local threshold subtracts the local average anyway. Whatever tone you lean a module towards, a block fully inside it will measure that lean as its baseline and threshold it away. The only lever that works is the share of the module that is solid, which is why the balance control exists.

Newer decoders, and why that is not the end of it

Newer decoders are better at this. In the same sweep, jsQR — which uses a different binarisation strategy — read every configuration at every distance, and the scanners built into current phones are stronger still. But "most phones will manage" is not the same as "every reader will", and a printed code meets whatever reader happens to be pointed at it: an older phone, a point-of-sale scanner, a kiosk, an app that bundles a ZXing-derived library. The photo-heavy balance is therefore honest about its trade — the studio notes that modern phone scanners read it at any distance while older, simpler readers can struggle in extreme close-up — and the decision of where to sit on the balance depends on who will scan the code and from how far. It also depends on where a phone naturally ends up: nobody holds a phone at arm's length from a poster, but everyone brings it right up to a business card, and that is the case where a photo-heavy code and a simpler reader can meet.

What to do about it

If the code goes somewhere people scan from a normal distance — a poster, a table tent, a shelf label — the photo-heavy settings are fine; at that distance a module covers a handful of camera pixels and the blocks straddle boundaries as they should. If it goes somewhere people will put a phone against it — a small sticker, a business card, a screen — move the balance towards Code, so that more than half of each module is solid and the close-up effect disappears for every class of reader. Either way, print one and scan it with a phone. Not a screenshot on a monitor: the actual printed piece, at the actual size, from the distance people will actually use, and ideally with more than one phone. SWAPQR shows this warning in the studio itself rather than in the small print, because a code that fails in the field is worse than a plain code that works, and because the fix is a slider rather than a reprint if you catch it before the run.

Forced share of each moduleFar / normal (5–8 px per module)Close-up (10–28 px per module)
~20% (photo-heavy)ReadsZXing-class readers unreliable
~44% (middle)ReadsZXing-class readers unreliable
~60% and above (code-heavy)ReadsReads
Any setting, jsQR-class and current phone scannersReadsReads in the same sweep

Frequently asked questions

My phone reads the photo code from any distance. Why the warning?

Current phone scanners use stronger binarisation and read every configuration in our sweep. The warning is about older or simpler readers that threshold in 8×8 blocks, which a printed code may still meet.

Why does moving the phone away fix it?

Further away, each module covers fewer camera pixels, so a decoder's 8×8 block straddles several modules and its average sits between dark and light. Up close, a block falls inside one module and follows the dither texture instead.

Does raising the tone bias help in close-up?

No. A local threshold subtracts the local average, so any uniform lean inside a module is thresholded away. Only the share of the module that is forced solid changes the result.

What balance is safe for a business card?

Move it towards Code, so that more than about half of each module is solid. At that point ZXing-class readers read at every distance in our measurement, and the picture is still visible, just fainter.

Print the square once. Decide later where it goes. SWAPQR makes static QR codes for free, with every style option, no account and no watermark. A paid plan turns a code dynamic: the printed square stays the same while you change its destination, and you see how often it was scanned, by day and by device.

Make a QR code in the browser
Language: TR EN DE ES FR IT PT AR RU JA KO