Skip to main content

QR Code

SignChain embeds a QR code alongside each signature block. The QR encodes a verification URL that links the physical document to its blockchain proof.

URL Format

https://signchain.app/v/<base64url(txHashBytes)>#<base64url(key)>
└──── path param ──────┘ └── fragment ─┘
SegmentEncodingSizePurpose
txHashBytesBase64url (no padding)43 chars (32 bytes)Identifies the blockchain transaction
keyBase64url (no padding)22 chars (16 bytes)AES-128-GCM decryption key

The #fragment is never sent to any server (RFC 3986). The decryption key stays entirely client-side.

Example URL Breakdown

https://signchain.app/v/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA#BBBBBBBBBBBBBBBBBBBBBB
├── base URL (27 chars) ──┤├── tx hash b64url (43 chars) ──────────────────────────────┤ ├── key b64url (22 chars) ─┤

Total URL length: ~95 characters.

Version & Error Correction Selection

QR codes have 40 versions (1--40) and 4 error correction levels. Higher versions hold more data but produce denser (harder to scan) codes. Higher EC levels tolerate more damage but reduce data capacity.

Capacity Table (Binary Mode)

VersionModulesEC Level LEC Level MEC Level QEC Level H
V537x37106 bytes84 bytes60 bytes46 bytes
V641x41134 bytes106 bytes74 bytes58 bytes
V745x45154 bytes122 bytes86 bytes64 bytes
V849x49192 bytes152 bytes108 bytes84 bytes
V953x53230 bytes180 bytes130 bytes98 bytes
V1057x57271 bytes213 bytes151 bytes119 bytes

SignChain's Choice: V6 / EC Level M

  • URL size: ~95 bytes
  • V6/M capacity: 106 bytes -- fits with margin
  • Error correction: 15% damage tolerance (suitable for printed documents)
  • Module count: 41x41 -- compact enough for small signature blocks

The QR is generated with an explicit version to avoid the library auto-selecting a larger version:

QrCode::with_version(url.as_bytes(), Version::Normal(6), EcLevel::M)

Why Not Higher EC?

OptionCapacityFits ~95 byte URL?
V6/H58 bytesNo
V6/Q74 bytesNo
V6/M106 bytesYes
V5/L106 bytesYes (same capacity, smaller)

V6/M was chosen over V5/L because the extra error correction (15% vs 7%) is worth the slightly larger module count for printed documents that may get scratched or folded.

Quiet Zone

The QR specification requires a 4-module quiet zone (blank padding) around the QR pattern. The qrcode crate includes this in the rendered image.

For V6 (41 pattern modules + 2x4 quiet zone = 49 total modules):

Quiet zone fraction = 4 / 49 ≈ 8.2%

This matters for positioning the branding text relative to the actual QR pattern:

let quiet_zone_frac = 4.0 / 49.0;
let quiet_zone = qr_size * quiet_zone_frac;
let inner_qr = qr_size - 2.0 * quiet_zone;

Minimum Size

The QR code is rendered at a minimum of 34 PDF points (~12mm). Below this size, phone cameras struggle to resolve individual modules.

Module countMin size (pt)Module sizePixels at 300 DPI
49 (V6 + quiet zone)340.69 pt~2.9 px/module

For reliable scanning, each module should be at least 2--3 pixels at the scanner's resolution. At 34pt printed at 300 DPI, this gives ~2.9 pixels per module -- at the lower bound but workable for good cameras.

Rendering

The QR image is rendered as a grayscale (DeviceGray) PDF XObject:

  1. Generate QR using the qrcode crate at 570x570 pixels (high resolution for clean scaling)
  2. Flip horizontally (compensates for PDF coordinate system interactions)
  3. Compress with zlib (FlateDecode)
  4. Add as a single XObject, referenced by all placement content streams

Branding Text

Below the QR code, "Signed with SignChain" is rendered in light gray Helvetica, right-aligned with the QR pattern edge (accounting for quiet zone offset).