Skip to content

fix(resolver): give the printed QR its quiet zone - #244

Merged
LKSNDRTMLKV merged 2 commits into
mainfrom
fix/qr-quiet-zone
Sep 7, 2026
Merged

fix(resolver): give the printed QR its quiet zone#244
LKSNDRTMLKV merged 2 commits into
mainfrom
fix/qr-quiet-zone

Conversation

@LKSNDRTMLKV

Copy link
Copy Markdown
Member

The PNG this node serves at GET /dpp/{dppId}/qr carried no quiet zone. That
is the rendering an operator downloads to print onto a physical label.

The defect

ISO/IEC 18004 requires four modules of blank margin on all four sides of a QR
symbol. It is not decoration — the quiet zone is how a scanner finds the
symbol's edge, and a code printed flush to other artwork is out of spec.

Two renderers exist and they disagreed. dpp-render's SVG, embedded in the
passport page:

let quiet = 4u32; // quiet zone in modules
let total = (width as u32 + quiet * 2) * module_size;

dpp-resolver's PNG, served by the /qr route:

let img_size = width * scale;   // no quiet zone at all

The divergence ran the wrong way round. The screen rendering — which a browser
surrounds with white page anyway — was the compliant one. The downloadable PNG,
the only output that ends up on a product, was the bare symbol.

Why nothing caught it

The route's only coverage was qr_endpoint_returns_png, which asserts a PNG came
back. A solid white square satisfies that.

The reason it stayed that way is structural: PNG generation was inline in
resolve_qr_handler, reachable only after a vault fetch, a Redis lookup and DID
verification. Testing what it encoded meant standing all three up, so the
affordable assertion was is_a_png — and that is what shipped, through a
sectorproductGroup rename and every dependency bump since.

qr_png is now a free function. That is what makes the rest of this testable.

The quiet zone is one value now

dpp_render::QR_QUIET_ZONE_MODULES, read by both renderers.

The renderers themselves stay separate. lib.rs records that they are split by
output class and that print-grade carrier work may collapse them into a carrier
crate later; that decision is somebody's to make and this PR does not pre-empt
it. Only the property neither is allowed to get wrong is shared.

A round-trip decode alone would not have caught this

Worth stating plainly, because it is the obvious way to write this test and it
does not work.

Measured, not assumed. With the fix reverted to quiet = 0:

the_png_encodes_the_carrier_uri_it_was_given ... ok      <- decode passes
the_symbol_keeps_its_quiet_zone_on_every_side ... FAILED
    (0,0) lies in the 32px quiet zone and must be blank

rqrr reads a symbol with no quiet zone perfectly well. Software decoders are
far more lenient than a hand scanner against a busy background, so a test that
only asks "does this decode?" goes green against the exact defect it was written
to find.

So the geometry is asserted directly — a blank border of the expected thickness
on all four sides — and the decode covers the payload. Neither is sufficient
alone.

Tests

dpp-resolver (new dev-dependency rqrr, test-only):

  • the_png_encodes_the_carrier_uri_it_was_given — decodes the PNG and asserts
    the payload equals the carrier URI. This is what a qrcode or image bump
    could previously have changed silently.
  • the_symbol_keeps_its_quiet_zone_on_every_side — the geometric assertion
    above.
  • the_quiet_zone_check_cannot_pass_on_a_blank_image — the margin check is
    satisfied by an all-white image, so this pins that dark modules exist.
  • an_unencodable_uri_is_an_error_not_a_wrong_symbol — a URI too long for any
    QR version errors rather than encoding something else.

dpp-render:

  • build_qr_svg_reserves_the_quiet_zoneviewBox exceeds the symbol by
    exactly two quiet zones, and no drawn module falls inside the margin.
  • build_qr_svg_encodes_the_carrier_uri is renamed to
    build_qr_svg_names_the_carrier_uri_in_its_title. It only ever checked the
    <title> text; the old name read as coverage that did not exist.

Bearing on #204 and #205

Both bump this exact path (qrcode 0.12 → 0.14.1, image 0.24 → 0.25.10) and
both passed CI on "bytes were returned". They should merge after this, so the
bumps are validated against what the label encodes rather than on faith.

Verification

just check green — 934/934, clippy -D warnings, fmt, the nine script
gates, integration compile, audit. Built against published dpp-core 0.19.0
(no .cargo/config.toml).

The quiet-zone assertion was confirmed to bite by reverting the fix and watching
it go red, as quoted above.

@codacy-production

codacy-production Bot commented Sep 7, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 54 complexity · 2 duplication

Metric Results
Complexity 54
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@LKSNDRTMLKV
LKSNDRTMLKV merged commit 54b4394 into main Sep 7, 2026
13 checks passed
@LKSNDRTMLKV
LKSNDRTMLKV deleted the fix/qr-quiet-zone branch September 7, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant