Why decorated QR codes still scan

A decorated QR code looks like a contradiction. A scanner wants a clean grid of dark and light squares; a keepsake wants hearts, a photograph, a frame. This guide explains, mechanism by mechanism, why the codes made on this site keep scanning after decoration — using the engine's own numbers. Every engine figure below is computed when the page is built, by the same functions that render the interactive generators, so the explanation cannot drift from the tool it describes; the print-size floor and the recovery share restate the methodology page.

The short version: the engine never touches the parts of a code a scanner locks onto, it encodes every style that redraws modules at the highest error correction a QR code can carry, it always draws the blank margin a decoder needs, and it checks its own work by decoding every render before a download is allowed. The rest of this page takes those four rules in turn, then covers three ways a verified code can be broken after it leaves the page.

What a scanner locks onto

A scanner finds a QR code by its three large corner squares, the finder patterns, and orients a denser code by the smaller squares inside it, the alignment patterns. Our testing showed that restyling alignment patterns breaks decoding outright, so in every KeepsakeCodes style the finder and alignment patterns are rendered as classic, crisp squares. The engine makes that rule mechanical rather than editorial. A function called classifyModule assigns every cell of a symbol to one of 4 zones — finder, alignment, timing, data — and each artistic style asks it before deciding how to draw a cell. A finder or alignment cell is always a plain square. A timing cell (the row and column the classifier marks as timing, index 6, which run between the finder patterns) may be styled: the heart style redraws it as a heart, while the photo halftone style keeps it pure. Only data cells carry decoration freely.

The alignment positions are not hard-coded either. alignmentPatternPositions mirrors the vendored encoder's own table, so the classifier can never disagree with the symbol that was actually drawn. Version 1, the smallest symbol, has no alignment pattern at all; from version 2 up there is at least one, and the count rises in steps rather than with every version: the table keeps 2 centres per axis from version 2 until version 7, where it moves to 3.

The anatomy of 6 real codes

The table below was produced by running classifyModule over every cell of 6 symbols: a fictional 4-character placeholder, and the build-time examples from 5 occasion pages. Each example is the exact payload shown on its page, encoded at level H as the generator encodes it. “Centres” lists what alignmentPatternPositions returns for that version; the 4 zone columns count cells; the last column is the share of the grid that is finder or alignment and therefore never restyled.

Symbol Version Grid Centres Patterns Finder Alignment Timing Data Landmarks
“KEEP” (placeholder) 1 21×21 none 0 147 0 14 280 33%
Photo code example 3 29×29 6, 22 1 147 25 30 639 20%
Proposal example 4 33×33 6, 26 1 147 25 38 879 16%
Wedding example 5 37×37 6, 30 1 147 25 46 1151 13%
Valentine example 8 49×49 6, 24, 42 6 147 150 60 2044 12%
Pet tag example 10 57×57 6, 28, 50 6 147 150 76 2876 9%

Three things are visible in the numbers. The finder count is the same in every row — three corners of 7×7 cells, 147 in all — because finder patterns do not grow with the version. Each alignment pattern occupies 5×5 cells, and the three corners that hold finder patterns carry none, which is why a version with 3 centres per axis yields 6 patterns rather than 9. And the landmarks' share of the grid falls as the symbol grows: from 33% of the smallest symbol to 9% of the version-10 pet tag — though a larger symbol also has smaller cells at any given print size, which is why the printing section below sizes a code by its module count.

A worked example: hearts around untouched landmarks

The valentine preset's example, rendered when this page was built by the same engine as the generator. Its message is fictional:

Every love story is beautiful, but ours is my favorite. Happy Valentine’s Day, A.

Version 8 at 49×49 modules, error correction H. Of its 1263 dark modules, 201 sit inside finder or alignment zones and are drawn as plain squares; the other 1062 are hearts spanning 0.97 of their cell. The test suite decodes this exact example with the same loop the generator runs.

Look at the three corners and the 6 small squares scattered through the hearts: those are the cells the classifier reserved. Everything else was free to be redrawn, and the heart glyph is sized so that a scanner's sampling grid still reads each cell as decisively dark. The heart-module guide covers that coverage figure and the colour gate in detail.

Maximum error correction wherever a module is redrawn

QR codes carry redundancy so that damaged modules can be reconstructed. Plain codes often use the middle levels to stay small; artistic codes spend that redundancy on decoration, so every style here that redraws modules — the hearts and the photo halftone — encodes at level H, roughly 30% recovery, and the engine rejects a model below H for them. The cost is visible in the symbol size. Asked for level M, the encoder returns the valentine message as a version-5 symbol at level M, 37×37 modules; at H the same message needs version 8, 49×49. Those extra modules are what the hearts and the photo halftone are drawn against.

The rule is enforced inside the engine, at the point where modules are redrawn, rather than in the form. Handed the level-M model, the heart style answers:

Artistic styles require error correction level H — re-encode with ecLevel "H".

The photo halftone style refuses the same way. The framed styles are the one exception, and by construction: a frame places the code untouched on a clean panel and draws around it, so nothing spends the code's own margin. The engine therefore lets a framed code carry level Q as well as H, and rejects anything below Q. The generator offers that as an opt-in “Smaller code (less error correction)” setting for frames without heart modules; asked for level Q, the encoder returns the wedding example as a version-4 symbol at level Q, 33×33 modules, where level H needs version 5, 37×37. A level-Q model can never become heart or halftone art, because those styles reject it first, and a render at either level still has to decode at 2 of the 3 raster sizes before it can be downloaded. See the framed-codes guide and the photo halftone guide for each style's own measured limits.

The quiet zone is part of the code

The quiet zone is the blank margin, 4 modules wide on each side, that lets a decoder tell where the symbol ends. The engine always renders it and offers no way to switch it off; in the framed styles the code sits, quiet zone included, on an opaque panel drawn above the frame art, so decoration cannot reach it by construction. For the valentine example the margin takes the artwork from 49 to 57 modules across, which means 26% of the finished square is deliberately empty. That emptiness is easy to treat as spare space when the artwork is laid out on a card, but a code that verified on screen can still fail in print if the margin is trimmed — keep the quiet zone uncropped.

Contrast: a scanner sees black and white

Scanners binarize the image before reading it, so a foreground that is only slightly darker than its background measurably hurts decoding. Every style that colours modules therefore requires the pair to measure at least 4.5:1 in WCAG contrast, computed from the chosen colours rather than guessed. The valentine preset's heart colour, #9f1239 on white, measures 8.02:1 and passes. A pale pink such as #f9a8d4 on white measures 1.81:1, and the engine rejects it rather than warning:

Artistic styles require at least 4.5:1 contrast (got 1.81:1) — decoration already spends decode margin.

Inverted pairs — light modules on a dark background — are refused outright, whatever their ratio: artistic styles require dark modules on a light background.

Verification closes the loop

Rules make failure unlikely; verification makes it visible. After each render, your browser rasterises the finished artwork — frame, hearts, photo and all — at 800, 1200, 1600 px and runs a real QR decoder (jsQR) on each raster, comparing the decoded bytes with your exact content. The “Verified scannable” badge is earned when at least 2 of the 3 rasters match; anything else shows a clear warning and keeps the downloads locked, so an unverified render cannot leave the page as a finished keepsake. Several sizes are used because a render can fail at exactly one raster size through sampling resonance and decode cleanly at the others — a single raster could report a false failure. The decode-verification guide walks through the rule and its outcomes.

What verification cannot see

On-screen verification proves the artwork decodes cleanly at screen resolution. It cannot see your printer, your paper, or the phone that will do the scanning. Print at least 2 × 2 cm for arm's-length scanning, keep the quiet zone uncropped, and scan a test print with a real phone before the moment that matters. The engine also keeps its own floor: it treats 0.5 mm as the working minimum for a printed module, so the valentine example's 57-module square comes out as “print at least 28.5 mm (1.12 in) wide”, and the 65-module pet tag as “print at least 32.5 mm (1.28 in) wide”. Past 65 total modules the engine flags a symbol as too large for a tag or card and asks for a shorter message instead. The printing guide turns these figures into sizes for each download preset.

Three ways a verified code gets broken

  • Cropping the quiet zone. The 4-module margin is read, not decorative. Lay the artwork out with its margin intact — keep the quiet zone uncropped.
  • Expecting bare text to display. A code containing bare text carries no scheme, so a phone has nothing to act on, and a keepsake cannot depend on which phone opens it (the FAQ below says what each does). That is why message keepsakes offer a reveal link, and why, on a framed text keepsake with the caption left empty, the message is printed under the code as its caption — the message-keepsakes guide explains both modes.
  • Low-contrast heart colours. A soft pink that looks right on a card can fall under the 4.5:1 gate, as the pair above shows. The engine refuses the render, so the fix is always the same: darken the hearts or lighten the background until the measured ratio clears the threshold.

Frequently asked questions

Which parts of a QR code does this site never decorate?

The finder patterns (the three 7×7 corner squares) and the alignment patterns (the smaller 5×5 squares inside denser codes). Restyling alignment patterns broke decoding outright in our testing, so every style draws both as classic squares. Timing cells may be styled, and the 4-module quiet zone is always drawn and never covered by frame art.

Why do the heart and photo halftone styles insist on error correction level H?

Level H carries roughly 30% recovery, and a style that redraws modules spends that margin on decoration, so the engine rejects a model below H for hearts and for the photo halftone. The cost is size: the valentine example needs a version-8 symbol at H where level M would need version 5. A frame leaves the code untouched on its panel, so the framed styles may also carry level Q through the generator's opt-in “Smaller code (less error correction)” setting — the wedding example drops from version 5 to version 4 — and every render at either level must still decode at 2 of the 3 raster sizes before download.

Can I crop the white margin to make the code fit?

No. The quiet zone is 4 modules wide on every side and is part of what a decoder reads; the engine always renders it and the framed styles keep it on an opaque panel above the frame art. A code that verified on screen can still fail in print if the margin is cropped — keep the quiet zone uncropped.

My phone scanned a code but showed nothing — is the code broken?

A code containing bare text carries no scheme, so a phone has nothing to act on: Apple’s Camera and Google’s ML Kit recognise links, phone numbers, email addresses, contact cards, calendar events and Wi-Fi joins, and no plain-text type. An iPhone offers to copy or search the text and Google Lens runs a web search; Samsung phones and dedicated scanner apps do display it. That is why a message keepsake can use a reveal link instead, and why, on a framed text keepsake with the caption left empty, the message is printed under the code as its caption; a caption you type takes its place.

How does the site know a decorated code still scans?

After every render your browser rasterises the finished artwork at 800, 1200, 1600 px and runs a real decoder (jsQR) on each raster, comparing the decoded bytes with your exact content. At least 2 of the 3 rasters must match to earn the “Verified scannable” badge; anything else shows a warning and keeps the downloads locked.

Does verification guarantee the code will scan in print?

No. Verification proves the artwork decodes cleanly at screen resolution; it cannot see your printer, your paper, or the phone that will do the scanning. Print at least 2 × 2 cm for arm’s-length scanning, keep the quiet zone uncropped, and scan a test print with a real phone before the moment that matters.

Every render is decode-verified in your browser before a download is allowed. The rules on this page — the anatomy rule, level H, the quiet zone and the contrast gate — are set out with their measured limits on the methodology page, and the other guides take each style in turn.