facecard.ing

How it works

How a picture fits inside a QR code

QR codes reserve up to 30% of themselves for error correction. Here is how that redundancy gets spent on an error-diffusion dither of your photo without breaking the scan.

A QR code is mostly slack

Every QR code is encoded with Reed–Solomon error correction, a scheme that adds redundant symbols so a damaged message can still be reconstructed. There are four levels — L, M, Q and H — recovering roughly 7%, 15%, 25% and 30% of the code respectively.

That budget exists because the physical world is unkind to printed codes: a thumbprint, a crease, glare from a phone flash, a sticker curling off a lamppost. A scanner is expected to read through all of it. If you generate at level H and the code is going to live on a clean screen, most of that budget is simply never spent.

Spending redundancy on a picture is the same trick a logo overlay uses — only instead of covering a patch in the middle, the image is spread across the entire code.

Every module gets a margin

A scanner does not read a QR module as a whole square. It samples near the centre of each one and decides black or white from that. Everything else in the cell is slack the decoder never looks at directly.

So each module is rendered several pixels wide. The middle is pinned hard to the module’s true value — that part is non-negotiable, it is the data. The ring around it is free, and that ring is where the photograph goes.

Error diffusion does the rest

The photo is reduced to one bit per pixel. Naive thresholding — anything above 50% grey becomes white — destroys a face, because skin sits in a narrow band of midtones that all collapse to one value.

Error diffusion fixes that. For each pixel you pick the nearest available colour, measure how far off you were, and push that error into the neighbours you have not visited yet. They are nudged darker or lighter to compensate, so across a region the average brightness stays true even though every individual pixel is a lie. That is why a dithered face reads as a face: the tone is carried by the density of dots rather than by any one of them.

The kernel decides how the error is shared out. Floyd–Steinberg gives it to four neighbours and preserves the most detail. Atkinson passes on only three quarters and discards the remainder, which lifts contrast and blows out highlights — the Macintosh look, and usually the most flattering on a portrait. Jarvis–Judice–Ninke and Stucki spread the error three rows deep instead of one, which reads as finer, more even texture and holds up better when the code is printed small.

Then the two are reconciled

The dithered image and the code are combined, with the module centres winning wherever they disagree. The finder patterns — the three big squares in the corners — stay solid by default: a scanner uses those to locate and orient the code before it reads anything else, and damaging them leaves little for error correction to recover. Switch off Solid finder patterns in the controls if you want the raw look instead, though that is the one setting most likely to cost you a scan. The timing lines and format strips are more expendable, and by default they aren’t held solid either — the dither runs through them the same as any other module, trading a little lock-on speed for a little more picture. Switch off Dither timing & format in the controls if you’d rather keep those solid too.

Nothing here decodes the result before handing it back to you. What keeps it scannable is the reconciliation above: the module centres stay untouched by default, and only the slack around them is ever given to the picture. Push the break threshold past what a preset sets and that stops being guaranteed, so test a code on your own phone before you print it.

Why it runs in your browser

All of the above is arithmetic over a pixel buffer, which is what a <canvas> is for. There is no reason to send a photograph of your face to a server to do it, so this doesn’t. The image never leaves the machine you are reading this on.