import QRCode from "qrcode"; /** * The single QR encoder for this project. * * ONE encoder feeds two surfaces — the SVG badge on screen and the vector PDF — so a * badge can never be printed with a different code than the one displayed. That drift * is the one failure this whole feature must not have: a badge whose printed code * differs from its on-screen code identifies the wrong person. * * `qrcode` is used rather than `react-qr-code` because we need the raw MODULE MATRIX, * not a rendered widget: the PDF draws each dark module as a real rectangle, which * keeps the printed QR vector-sharp at any printer resolution. */ // High correction on purpose: a badge is scanned off a lanyard, and the realistic // failure modes are a scratch, a smear of mud, or a fold. H tolerates ~30% damage. const EC_LEVEL = "H" as const; export type QrMatrix = { /** Modules per side, e.g. 29. */ size: number; /** Row-major, 1 = dark. */ data: Uint8Array; }; export function qrMatrix(payload: string): QrMatrix { const qr = QRCode.create(payload, { errorCorrectionLevel: EC_LEVEL }); return { size: qr.modules.size, data: qr.modules.data }; } /** Is a module at (x, y) dark? */ export function isDark(m: QrMatrix, x: number, y: number): boolean { return m.data[y * m.size + x] === 1; } /** * A QR as an inline SVG path, with the quiet zone baked in. * * `d` is a run of `M x y h w v h ... z` rectangles — one per dark module, each a * single filled square. One path element instead of hundreds of s keeps the DOM * small when a page shows a badge per mechanic. */ export function qrSvgPath(m: QrMatrix, quiet = 2): { d: string; viewBox: number } { const span = m.size + quiet * 2; const parts: string[] = []; for (let y = 0; y < m.size; y++) { let run = 0; for (let x = 0; x <= m.size; x++) { const dark = x < m.size && isDark(m, x, y); if (dark) { run++; } else if (run > 0) { // Merge horizontally adjacent dark modules into one rect — far fewer path // commands, and visually identical. parts.push(`M${x - run + quiet} ${y + quiet}h${run}v1h-${run}z`); run = 0; } } } return { d: parts.join(""), viewBox: span }; }