Methodology & Accuracy

Every keepsake on this site is produced by a small, tested rendering engine, and every rule in that engine exists because we measured what happens when it is broken. This page documents those rules — what gets styled, what never does, and the limits we verified with a real decoder — so you can judge exactly how far to trust a decorated code.

This page is the reference; its rules are worked through one at a time by 8 guides, on figures the engine computes when the site is built. The list below and the guides index are read from the same source, so the count and the links cannot disagree:

The anatomy rule: landmarks are never decorated

A scanner finds a QR code by its three large corner squares (finder patterns) and orients dense codes with smaller inner squares (alignment patterns). Our testing showed that restyling alignment patterns breaks decoding outright — so in every KeepsakeCodes style, finder and alignment patterns are always rendered as classic, crisp squares. Timing tracks may be styled; the landmarks may not. The mandatory four-module quiet zone (the clear margin around the code) is always drawn and is never covered by frame art.

Maximum error correction wherever a module is redrawn

QR codes carry redundancy so damaged modules can be reconstructed. Plain codes often use the middle levels to stay small; artistic codes spend that redundancy on decoration. Every style here that redraws modules — hearts and the photo halftone — therefore encodes at level H (roughly 30% recovery), and the engine refuses to render those styles over a lower level. A frame leaves the code untouched on its panel, so a framed code without heart modules may also carry level Q when the generator's opt-in smaller-code setting is chosen; the engine rejects anything below Q, and a render at either level must still pass decode verification before it can be downloaded.

Heart-module codes

Dark modules are redrawn as heart glyphs spanning 0.97 of their cell — large enough that a scanner's sampling grid still reads each cell as decisively dark, small enough that adjacent hearts read as separate shapes to the eye. Foreground and background must measure at least 4.5:1 in WCAG contrast; the engine rejects weaker pairs rather than warning, because scanners binarize the image and low contrast measurably hurts decoding.

Photo halftone codes

Each data cell is split into a small grid. The center half of the cell is authoritative: it is rendered in the pure module color, because decoders sample near cell centers. Our measurements fixed this at 50% of the cell — at 40%, decoding fails. The outer ring of the cell displays your photo, quantized to three tones (dark, mid, light) at luminance thresholds of 85 and 175 on the 0–255 scale, so the picture stays visible without inventing false modules. Finder, alignment, and timing cells plus the quiet zone stay fully pure, and no logo may be stacked on a halftone code — the photo already spends the decode margin.

Framed codes

The safest style by construction: the code itself is not modified at all. It sits, quiet zone included, on an opaque clean panel drawn above the frame art, so decoration can never touch a module. Frames (heart, floral, ring, classic, confetti) are recolorable vector art; changing the palette cannot affect the code.

Every render is decode-verified

Rules make failure unlikely; verification makes it visible. After each render, your browser rasterizes the finished artwork — frame, hearts, photo and all — and runs a real QR decoder (jsQR) on the pixels, then compares the decoded bytes to your exact content. A match shows the "Verified scannable" badge; anything else shows a clear warning and keeps the downloads locked, so an unverified render cannot leave the page as a finished keepsake.

Tested the same way, automatically

The engine's test suite runs the identical render-rasterize-decode loop on every style — at several raster sizes, rotated, and after lossy JPEG re-compression — and requires the decoded bytes to match exactly. The measured limits above (the 0.97 heart coverage, the 50% halftone center, the alignment-pattern rule) are pinned by these tests, so a future change that breaks scannability fails the build instead of shipping.

Worked examples can't drift

The example codes shown on tool pages are rendered at build time by the same engine that powers the interactive generators — never pasted-in images. If the rendering logic changed, the page art would change with it (or the build would fail), so the documentation cannot silently disagree with the tool.

How a message keepsake is encoded

A QR code containing bare text carries no scheme, so a phone has nothing to act on. Apple's Camera and Google's ML Kit both recognise links, phone numbers, email addresses, contact cards, calendar events and Wi-Fi joins — and no plain-text type. Faced with bare text, an iPhone offers to copy or search it and Google Lens simply runs a web search, so the message is the one thing the reader never sees. Samsung phones and dedicated scanner apps do display it, but a keepsake cannot depend on which phone opens it.

So a message keepsake offers two honest modes. A reveal link encodes a normal URL to a page on this site, with the message itself carried in the URL's fragment — the part after the #. Per RFC 3986, a fragment is stripped before a request is made, so it never reaches our servers, a CDN, or any log; the message is decoded entirely on the reader's device. An offline note writes the words into the code itself, needing no connection ever, and accepts the display caveat above. In both modes, on a framed keepsake with the caption left empty, the message is also printed under the code as its caption, so it can be read by eye regardless; a caption you type takes its place.

The fragment format is published here so the codes stay readable by anyone, forever, with or without this site: # then a single flag character (A for UTF-8 bytes, B for raw-deflate compressed bytes), then the payload in unpadded uppercase base32 (RFC 4648), then one base32 checksum character over the payload. Base32 is used rather than base64 because some scanners change the case of what they read, and base32 survives that; it is also a subset of the QR alphanumeric character set, which a mixed-mode encoder can pack at 5.5 bits per character instead of 8 (this site's encoder picks one mode for the whole link and, because the scheme and the # fall outside that set, encodes the entire link in byte mode — see the reveal-link worked figure on this page). Every message keepsake can additionally be downloaded as a self-contained HTML file that decodes this format with no network and no website at all.

Constants

The rules above quote numbers. This table pins each one to the constant it comes from in the engine's source. The values are read from that source when the site is built, not typed into this page, so the page cannot quote a limit the engine no longer enforces: if a constant moved, the table would move with it.

Constant Value What it pins
HEART_CELL_COVERAGE 0.97 Share of its cell a heart glyph spans in a heart-module code.
HALFTONE_CENTER_RATIO 0.5 (50% of the cell side) The authoritative pure-colour centre of a halftone data cell.
HALFTONE_LUM_DARK / HALFTONE_LUM_LIGHT 85 / 175 Three-tone luminance thresholds for the photo ring: dark below the first, light above the second.
HALFTONE_MAX_VERSION 4 Highest symbol version a halftone code may use; denser content is refused.
HALFTONE_MAX_CHARS 34 Message characters that always fit a halftone code (fewer with emoji or accents).
HALFTONE_MAX_INSCRIPTION_CHARS 50 Characters that fit the same code when restricted to the QR alphanumeric set.
FRAME_TEMPLATES heart, floral, ring, classic, confetti The recolourable frame templates.
CAPTION_MAX_CHARS / CAPTION_MAX_LINES 200 / 4 Longest caption the frame will set, and the most lines it may wrap to (a caption typed in the generator is limited by its form; the message-as-caption fallback may use the full budget).
QUIET_ZONE_MODULES 4 Quiet-zone width in modules on every side; always drawn.
CONTRAST_WARNING_THRESHOLD 4.5:1 Minimum foreground/background WCAG contrast; artistic styles reject below it.
VERIFY_RASTER_SIZES 800, 1200, 1600 px The 3 raster sizes every verification decodes at.
VERIFY_MIN_PASSES 2 Rasters that must decode to an exact byte match before the badge is earned.
FLAG_RAW / FLAG_DEFLATE A / B The reveal fragment's flag character: UTF-8 bytes, or raw-deflate first.
REVEAL_PATH /m/ Path of the reveal page a reveal link points at.
REVEAL_MAX_CHARS 300 Longest message a reveal link accepts, before encoding.
HUNT_MIN_CLUES / HUNT_MAX_CLUES 2 / 20 Fewest and most clues a treasure-hunt chain accepts.

Worked figures

The figures below are produced when the site is built, by the same functions the generators run, from fictional inputs chosen for this page. A figure that could not be produced would fail the build rather than print a stale number.

A reveal fragment

Take a fictional message, Same bench, same river, forty years on. - J: 43 characters, 43 UTF-8 bytes, inside the 300-character limit. Called the way the generators on this site call it — with no compressor — the engine base32-encodes those bytes as they are, so the flag is A. The fragment is AKNQW2ZJAMJSW4Y3IFQQHGYLNMUQHE2LWMVZCYIDGN5ZHI6JAPFSWC4TTEBXW4LRAFUQEUV — flag A, a 69-character base32 payload KNQW2ZJAMJSW4Y3IFQQHGYLNMUQHE2LWMVZCYIDGN5ZHI6JAPFSWC4TTEBXW4LRAFUQEU, and the checksum character V — 71 characters in all, every one of them in the QR alphanumeric set. Lower-casing the entire fragment and decoding it returns the message unchanged, which is what base32 buys.

Flag B is the other half of the published format. The encoder takes a raw-deflate compressor as an optional argument; given one, it encodes both ways and keeps whichever base32 payload is shorter — the decision is made on payload length, not on byte count. Handed a compressor when this page is built, the same message compresses from 43 bytes to 42: the raw base32 payload is 69 characters and deflate's is 68, so the compressed encoding wins by 1 character and the fragment becomes BBNHMYTKVJBFM2S6O2BISQBVRRMZMWUULOQKNF4ULJIVBKKSTCOFYUFPS6P2BI5AVXQAAH, which also decodes back to the message after lower-casing. On a message this short the saving is marginal, and on a shorter one the deflate header costs more than it removes: handed the same compressor, a fictional 12-character message, Back at six., deflates from 12 bytes to 14 — more bytes, so a longer base32 payload — and the encoder keeps the raw encoding, flag A: AIJQWG2ZAMF2CA43JPAXAH. That comparison is why the format carries a flag at all. The generators do not pass a compressor today, so every reveal link they produce carries flag A; the reveal page parses either flag and inflates a flag-B payload with the platform before reading it, so a future compressed link needs no new format.

The full link, https://KEEPSAKECODES.NET/M/#AKNQW2ZJAMJSW4Y3IFQQHGYLNMUQHE2LWMVZCYIDGN5ZHI6JAPFSWC4TTEBXW4LRAFUQEUV, encodes at level H as version 10: 57×57 modules, 65 across with the quiet zone. Those sizes are what this site's encoder produces: it picks one encoding mode for the whole string, and because the link contains characters outside the QR alphanumeric set — the lowercase scheme and the # — the entire link, fragment included, is encoded in byte mode. buildRevealPayload also publishes the segment plan a mixed-mode encoder would use: 4 runs for this 100-character link, with 91 characters in the alphanumeric runs (the uppercased host and path, and the fragment, every character of them inside the set noted above) and 9 in the byte runs (the scheme and the #), and concatenating the runs reproduces the link exactly. The same link at level Q — the opt-in a framed code without heart modules may take — is version 8, 49×49 modules, 57 across: 2 versions smaller for the same link.

A hunt chain

A fictional draft of 5 lines — one blank, one with stray spaces — is split into 4 clue texts: one clue per line, blank lines dropped, each line trimmed. The chain builder numbers them in order. The first card is captioned Clue 1 of 4; the last is captioned The prize · 4 of 4 and marked final, so it can be printed as the prize rather than as another step. A chain accepts between 2 and 20 clues; a one-clue list is refused with: A hunt needs at least 2 clues — the first one and whatever it leads to.

Card Caption Clue length Symbol as a reveal link
1 Clue 1 of 4 27 characters version 8, 49×49 modules, 57 across
2 Clue 2 of 4 39 characters version 9, 53×53 modules, 61 across
3 Clue 3 of 4 35 characters version 9, 53×53 modules, 61 across
4 The prize · 4 of 4 40 characters version 9, 53×53 modules, 61 across

Each card is a separate render and is decode-verified on its own; the chain decides only what each card says and carries. Clue length sets symbol size in steps, not smoothly: the longest clue here, 40 characters on card 4, encodes as version 9, 61 modules across, the widest in this chain — and cards 2, 3 and 4 all share that width: the encoder chose version 9 for clues of 39, 35 and 40 characters alike.

The contrast gate

The gate is 4.5:1 by the WCAG 2.x formula, measured on the actual colours. The valentine preset's heart colour, #9f1239, measures 8.02:1 on white, and the heart renderer accepts it. A fictional pale pick, #fbcfe8, measures 1.38:1; the renderer returns an error rather than a warning, and this is its exact message: Artistic styles require at least 4.5:1 contrast (got 1.38:1) — decoration already spends decode margin. Nothing is rendered, so nothing can be verified or downloaded.

The halftone budget

The halftone style is capped at version 4, and the two character limits in the table follow from that cap rather than sitting beside it. A fictional message of exactly 34 plain characters, Our first garden, planted together, encodes at level H as version 4: 33×33 modules, 41 across with the quiet zone. Add one character — a full stop — and the same words need version 5; handed that model, the halftone renderer returns an error rather than a render, and this is its exact message: This content needs a version-5 code, and photo weaving is decode-proven only to version 4 (about 34 characters, fewer with emoji or accents). Three ways forward: pick a different style, use a short link instead of a message, or switch to an inscription — uppercase letters, digits and simple punctuation fit 50 characters in the same photo code. Restricting the words to the QR alphanumeric set, 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:, changes the encoding mode rather than the version: a fictional inscription of exactly 50 such characters, OUR FIRST GARDEN PLANTED TOGETHER ONE MAY MORNING., encodes as version 4 too, 33×33 modules.

Print size

Two readouts, both from the engine, answer different questions. The first describes a PNG raster: at 300 DPI a long side of 512 px prints 43.3 mm, 1024 px prints 86.7 mm and 2048 px prints 173.4 mm, for the engine's export presets. The generator rasterises the verified SVG so that its longer side measures the chosen pixel count (its picker offers 1024 px and 2048 px), so on a framed keepsake that figure is the long side of the whole composition, frame art included, and not the code panel. The second prescribes a floor: every module must print at least 0.5 mm wide, quiet zone included, so the reveal-link example above, 65 modules across, carries the label print at least 32.5 mm (1.28 in) wide, and a symbol wider than 65 modules is flagged as oversized (the example, at 65, is not). A halftone code confines its authoritative module colour to a centre square 50% of the cell side, and the engine counts only that centre as the effective module, so the per-module floor is divided by that share: the version-4 symbol above, 41 across, would need 20.5 mm as a plain or framed code and 41.0 mm as a halftone. These floors are the engine's per-code minimums; the 2 × 2 cm figure below is this page's general guidance; and both describe the nominal size only: the test print is still the only measurement that includes your printer.

What verification cannot promise

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. If a verified code will not scan in print, that is exactly the report we want — see the contact page; confirmed issues are fixed in the engine and locked in with a new test.

Last reviewed 2026-09-30. Every figure in the Constants and Worked figures sections was recomputed when this page was built.