/** * peachQR.ts โ€” render a Peach binary payload as a printable QR code. * * The spec (ยง4) calls for a proprietary dense matrix format with custom * anchor patterns and Reed-Solomon ECC tuned for high-capacity payloads. * That decoder is non-trivial (perspective correction + ECC + camera * frame analysis) and is genuinely out of scope for v2.5.0. For now the * print-backup path uses a standard QR code via the bundled `qrcode` * package with byte-mode segments and low ECC for maximum capacity. * * Standard QR (v40, ECC level L) caps at 2,953 binary bytes โ€” that * covers the common case (vaults up to roughly 100 entries). Larger * vaults are advised to use PeachScript instead. The full proprietary * matrix code (20 KB capacity, custom Reed-Solomon, four-anchor * orientation) is a follow-up; the call sites below already separate * "encode" from "render", so swapping in the spec-compliant codec later * does not require touching the UI layer. */ import QRCode from "qrcode"; /** Approximate binary capacity for standard QR Version 40, ECC level L. */ export const QR_MAX_BYTES = 2953; export interface PeachQROptions { /** Pixel width of one matrix module. Higher = larger PNG. Default 6. */ scale?: number; /** White margin in modules around the code. Default 4. */ margin?: number; } /** * Render a binary payload to a black-and-white QR canvas. Throws * `QR_PAYLOAD_TOO_LARGE` if the payload exceeds standard QR capacity โ€” * callers should fall back to PeachScript in that case. */ export async function renderPeachQR( payload: Uint8Array, opts: PeachQROptions = {} ): Promise { if (payload.length > QR_MAX_BYTES) { throw new Error("QR_PAYLOAD_TOO_LARGE"); } const canvas = document.createElement("canvas"); await QRCode.toCanvas( canvas, [{ data: payload, mode: "byte" }], { errorCorrectionLevel: "L", margin: opts.margin ?? 4, scale: opts.scale ?? 6, color: { dark: "#000000", light: "#ffffff" }, } ); return canvas; } /** Convenience: PNG data URL ready for download. */ export async function renderPeachQRDataURL( payload: Uint8Array, opts: PeachQROptions = {} ): Promise { if (payload.length > QR_MAX_BYTES) { throw new Error("QR_PAYLOAD_TOO_LARGE"); } return QRCode.toDataURL([{ data: payload, mode: "byte" }], { errorCorrectionLevel: "L", margin: opts.margin ?? 4, scale: opts.scale ?? 6, color: { dark: "#000000", light: "#ffffff" }, }); }