Dithered QR codes: how a photograph becomes a scannable code
Most "artistic" QR codes are a picture with a code stamped on top. A dithered QR code is something else: there is only one image, and the code is part of its grain. This article explains what a code actually needs, how error-diffusion dithering works, the one change that lets the two share a square, the two settings that decide the result, and the three ways it fails.
Start with what a QR code actually needs
A QR code is a grid of modules — small squares that are either dark or light. Some of them are structural and cannot move: the three big squares in the corners that let a scanner find and orient the code, the thin alternating line between them, the small alignment squares, and the strips that encode format and version. Everything else is data plus error-correction bytes. That error correction is the opening. At the highest level, roughly 30% of the code can be destroyed and a reader will still recover the payload. So a code does not have to be perfect. It has to be perfect where it counts, and close enough everywhere else. Hold on to that distinction, because the whole technique rests on it: the structural modules are treated as untouchable, and the data area is treated as something that only needs to be right on average, at the scale a camera sees it. Every generator that puts a picture in a code is spending some of that 30% budget; the question is only how carefully it spends it, and whether the eye can see the cost.
Dithering, in one paragraph
Floyd–Steinberg dithering turns a greyscale photograph into pure black and white. You walk the pixels in order. For each one you pick the nearer of black and white, then take the error you just made — the difference between the grey you wanted and the value you printed — and push it into the neighbours you have not visited yet, weighted 7/16 to the pixel on the right and 3/16, 5/16 and 1/16 to the three pixels below. Each of those neighbours then starts from a slightly adjusted value when its own turn comes, so the error is never lost, only moved. The picture comes out as a field of speckle whose local density matches the original brightness: a mid-grey region becomes roughly half black dots, a highlight becomes sparse dots on white, a shadow becomes sparse white dots on black. Seen from a distance, the eye averages the speckle back into tone, which is why a dithered image reads as a photograph even though every pixel in it is one of only two colours. That averaging is the same thing a camera does when it looks at a small print, and it is what the technique exploits.
The trick
Render the code at a fixed number of pixels per module — SWAPQR uses nine, so every module is a 9×9 block. Then dither the photograph across the whole square, with one change: where the code requires a specific value, do not threshold. Force the pixel to what the code needs, measure the error that forcing caused, and diffuse it exactly as you would any other error. That last clause is the whole idea. The forced dots are not painted over the picture; they are paid for by the pixels around them, which shift to compensate. They land inside the same salt-and-pepper texture as everything else, so the eye has nothing to separate them from. Structural modules — finders, timing, alignment, format — are drawn solid and untouched, because a scanner has to find the code before error correction can help it. The technique described here follows the approach published by Andrew T., who wrote up dithered QR generation in 2026; SWAPQR's implementation was written from the description rather than ported from his source.
Two knobs that decide everything
The first is how much of each module is forced. Force only the centre pixel and the picture is almost untouched, but the code is fragile: one pixel per module has to survive every resampling step between the printer and the camera sensor. Force a 7×7 block out of the 9×9 and the code is robust and the picture is faint, because most of every module is now solid. SWAPQR exposes this directly as a Photo ↔ Code balance, with Photo, Middle and Code positions, so the trade is a choice you make rather than a constant hidden in the generator. The second is how far each module is nudged. Before dithering, every pixel of a dark module is darkened a little and every pixel of a light module lightened a little. The picture keeps its detail, but the module's average tone leans the right way — which is what a camera sees when it looks at a small printed code from across a room. Between them, the two settings decide whether the result is a photograph with a faint code in it or a code with a faint photograph in it, and where on that line a given print should sit depends on how it will be scanned.
Where it breaks, and whether it is worth it
Three failure modes, all real, none of them mysterious. A flat photograph: an evenly lit, single-tone image dithers into a regular chequerboard, which is exactly the texture that competes with the code, so pictures need a genuine light-to-dark range and the studio warns when one does not have it. A long link: more characters means more modules in the same square, so each module gets less picture; past about version 8 — 49×49 modules — the photograph has nowhere to live, and a short redirect link is the fix. And extreme close-up on some readers, which is counter-intuitive enough that it has its own article in this guide. An artistic code is always a trade against reliability. It earns its place where the code is part of the design and someone is going to scan it deliberately — a poster, an album sleeve, packaging, a business card. It does not belong on a fire-exit sign. Whatever the setting, test the printed piece with a real phone before you order a thousand of them; the studio says the same thing next to the export button, and means it.
| Part of the code | Treatment in a dithered code | Why |
|---|---|---|
| Finder patterns (three corner squares) | Drawn solid, never dithered | The scanner must locate and orient the code before anything else |
| Timing line, alignment squares, format and version strips | Drawn solid, never dithered | Structural; error correction cannot repair a code that is not found |
| Data and error-correction modules | A central block of each module forced, the rest carries the photo | The module's average tone is what a camera sees at scan distance |
| Pixels around a forced block | Shifted by error diffusion to compensate | Keeps the local average right so the forced dots hide in the speckle |
| Quiet zone | Light, untouched | Scanners use the empty margin to find the code's edge |
Frequently asked questions
Is the photo just placed behind the code?
No. The code and the photo are produced in one pass: the picture is dithered, and where the code needs a value the pixel is forced and its error is diffused into the neighbours. There is a single image, not a layer on top of another.
Why nine pixels per module?
It gives each module a 9×9 block, large enough to hold a forced core of variable size and still leave pixels for the picture. The balance setting decides how much of that block is forced.
Does it scan with ordinary phones?
Yes, for the payloads and images it is designed for, and every build is checked against two independent decoders at seven simulated camera distances. It is still a trade against reliability, so print one and test it with your own phone.
Why does a long link ruin the picture?
Longer text means a higher version, so more and smaller modules in the same square. Each module has less room for photo pixels, and past roughly 49×49 modules the picture disappears. Use a short redirect link.
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