diff --git a/Cargo.lock b/Cargo.lock
index 40e2fa48..54a6228f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -526,7 +526,7 @@ dependencies = [
"http 0.2.12",
"http 1.5.0",
"http-body 1.1.0",
- "lru",
+ "lru 0.18.2",
"percent-encoding",
"regex-lite",
"sha2 0.11.0",
@@ -2140,6 +2140,8 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpp-aas"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4ec4a1cc09b41cc32c1d5c40e595c541c0c6c6e52cec2670810524a7548123"
dependencies = [
"dpp-domain",
"dpp-vocab",
@@ -2150,6 +2152,8 @@ dependencies = [
[[package]]
name = "dpp-calc"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1f5a7897583c4fb6c43056506d83acfc71c1eb14a7d239dc71c154b2ce8d12d"
dependencies = [
"chrono",
"dpp-rules",
@@ -2215,6 +2219,8 @@ dependencies = [
[[package]]
name = "dpp-crypto"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1747716351031fbd56464d367ebfa5c2bc3b21d6bdbef10f6705a52a6bae2149"
dependencies = [
"aes-gcm",
"anyhow",
@@ -2263,6 +2269,8 @@ dependencies = [
[[package]]
name = "dpp-digital-link"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee158b22c553f128c436121d011c8c9f0f6af67a5a33a6594828e837c2024863"
dependencies = [
"dpp-domain",
"serde",
@@ -2273,6 +2281,8 @@ dependencies = [
[[package]]
name = "dpp-domain"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a19209f530263d1bfb75a86cc3b2cd54c058b87b57ff57e1b898072e4a7f452"
dependencies = [
"async-trait",
"chrono",
@@ -2442,6 +2452,8 @@ dependencies = [
[[package]]
name = "dpp-plugin-traits"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3379fa5d91c4f7d179bb92dd08cfebc605ba3d699d9cb3729f0910aeaf8888d"
dependencies = [
"semver",
"serde",
@@ -2452,6 +2464,8 @@ dependencies = [
[[package]]
name = "dpp-registry"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b626175826e18ef802f2e9de5da9ea449a202e1d34823e5771880fedefaf85b6"
dependencies = [
"chrono",
"dpp-domain",
@@ -2500,6 +2514,7 @@ dependencies = [
"rand 0.10.2",
"redis",
"reqwest",
+ "rqrr",
"serde",
"serde_json",
"thiserror 2.0.19",
@@ -2514,6 +2529,8 @@ dependencies = [
[[package]]
name = "dpp-rules"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bbedb7cce0dad9d136793248444eba53c305a2811168d1bf3a7f229d61b02ce"
dependencies = [
"base64 0.23.1",
"chrono",
@@ -2617,6 +2634,8 @@ dependencies = [
[[package]]
name = "dpp-vc"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8feb107d2fe5a7a4391e3dfb46cfaa8335e2b9ef678bec9774a1669e1fa58a1"
dependencies = [
"anyhow",
"async-trait",
@@ -2637,6 +2656,8 @@ dependencies = [
[[package]]
name = "dpp-vocab"
version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73c5b4713451f179e3f7bbfa09f19c94cfb3713709a43116328875ef44a03735"
dependencies = [
"serde",
"serde_json",
@@ -3143,6 +3164,34 @@ dependencies = [
"thread_local",
]
+[[package]]
+name = "g2gen"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a7e0eb46f83a20260b850117d204366674e85d3a908d90865c78df9a6b1dfc"
+dependencies = [
+ "g2poly",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "g2p"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "539e2644c030d3bf4cd208cb842d2ce2f80e82e6e8472390bcef83ceba0d80ad"
+dependencies = [
+ "g2gen",
+ "g2poly",
+]
+
+[[package]]
+name = "g2poly"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "312d2295c7302019c395cfb90dacd00a82a2eabd700429bba9c7a3f38dbbe11b"
+
[[package]]
name = "generator"
version = "0.8.9"
@@ -4063,6 +4112,15 @@ dependencies = [
"tracing-subscriber",
]
+[[package]]
+name = "lru"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
+dependencies = [
+ "hashbrown 0.16.1",
+]
+
[[package]]
name = "lru"
version = "0.18.2"
@@ -4853,7 +4911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [
"anyhow",
- "itertools 0.13.0",
+ "itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -5398,6 +5456,17 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "rqrr"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbe87d9e8db95652c25ded2418150e00b08c2fde09e23ec15896d2c470c6631"
+dependencies = [
+ "g2p",
+ "image 0.24.9",
+ "lru 0.16.4",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.28"
@@ -6325,7 +6394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
diff --git a/crates/dpp-render/src/carrier.rs b/crates/dpp-render/src/carrier.rs
index 90f180c7..5fdda90f 100644
--- a/crates/dpp-render/src/carrier.rs
+++ b/crates/dpp-render/src/carrier.rs
@@ -2,6 +2,35 @@
use serde_json::Value;
+/// The quiet zone every rendering of a carrier must leave around the symbol,
+/// in modules.
+///
+/// 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 what lets a scanner find
+/// the symbol's edge, and a code printed flush to other artwork — or to the
+/// edge of a label — is out of spec whether or not any particular decoder is
+/// lenient enough to read it anyway.
+///
+/// # Why this is a shared constant and not a number in each renderer
+///
+/// It was a number in each renderer, and they disagreed. The SVG rendered for
+/// the passport page applied four modules; the PNG served by the resolver's
+/// `/qr` route applied **none**, sizing its image at exactly `width * scale`.
+/// The divergence ran the wrong way round, too — the screen rendering, which a
+/// browser surrounds with white page anyway, was the compliant one, while the
+/// downloadable PNG an operator would actually print onto a label was the
+/// symbol with no margin at all.
+///
+/// Software decoders mostly tolerate a missing quiet zone, which is exactly why
+/// nothing caught it: a round-trip test that only asks "does this decode?" is
+/// satisfied by a symbol no hand scanner would read against a busy background.
+/// So the geometry is asserted directly, per renderer, against this constant.
+///
+/// The two renderers stay separate — they are split by output class, and
+/// `lib.rs` records the intent to revisit that — but the property neither is
+/// allowed to get wrong now has one home.
+pub const QR_QUIET_ZONE_MODULES: u32 = 4;
+
/// Build the GS1 Digital Link URI a carrier (QR/Data Matrix) for this
/// passport should encode.
///
diff --git a/crates/dpp-render/src/lib.rs b/crates/dpp-render/src/lib.rs
index 005f5cdf..02fe4698 100644
--- a/crates/dpp-render/src/lib.rs
+++ b/crates/dpp-render/src/lib.rs
@@ -25,5 +25,5 @@ mod page;
mod remainder;
mod sections;
-pub use carrier::carrier_uri;
+pub use carrier::{QR_QUIET_ZONE_MODULES, carrier_uri};
pub use page::{SnapshotNotice, build_qr_svg, render_page};
diff --git a/crates/dpp-render/src/page.rs b/crates/dpp-render/src/page.rs
index 020ba8c7..a88298a5 100644
--- a/crates/dpp-render/src/page.rs
+++ b/crates/dpp-render/src/page.rs
@@ -175,7 +175,7 @@ pub fn build_qr_svg(carrier_uri: &str) -> String {
let width = code.width();
let colors = code.to_colors();
let module_size = 4u32;
- let quiet = 4u32; // quiet zone in modules
+ let quiet = crate::carrier::QR_QUIET_ZONE_MODULES;
let total = (width as u32 + quiet * 2) * module_size;
let mut rects = String::with_capacity(colors.len() * 48);
@@ -337,13 +337,65 @@ mod tests {
assert!(html.contains("<script>"));
}
+ /// Renamed from `build_qr_svg_encodes_the_carrier_uri`, which claimed more
+ /// than it checked: the URI it found was the one in the `
`, not
+ /// anything about the modules. What the symbol encodes is covered by the
+ /// resolver's PNG round-trip; both renderings come from the same
+ /// `QrCode::new`, so the encoding is exercised once rather than twice.
#[test]
- fn build_qr_svg_encodes_the_carrier_uri() {
+ fn build_qr_svg_names_the_carrier_uri_in_its_title() {
let svg = build_qr_svg("https://id.odal-node.io/01/09506000134352/21/abc");
assert!(svg.starts_with("