import QRCode from "qrcode"; /** * QR matrix builder for documents printed by `@react-pdf/renderer`. * * ## Why the matrix, and not a PNG * * A QR could be embedded as an image, but a matrix renders as pure * VECTOR — one `` per dark module — which is strictly better * here: * * - it stays crisp at any print size or DPI, with no resampling * - it adds ~30 shapes rather than a compressed image blob, so the * PDF stays small * - it does not depend on react-pdf's raster image path at all. * That path was exactly what the old `pdf().toString()` pipeline * silently corrupted (see the note at the top of `render.tsx`), * and vector content is immune to it. * * ## What the QR carries * * Currently just the delivery note number, e.g. `DN26100401`. * * SECURITY NOTE — this is a deliberate product decision, not an * oversight. The number is a daily sequential counter (`DN{YY}{MM}{DD}{NN}` * starting at 01), so it is fully enumerable: anyone can guess every * delivery note that will ever exist. A QR carrying the bare number, * pointed at an upload endpoint, is therefore a signature-forgery * vector. * * That is acceptable *today* because the intended consumer is a * closed, logged-in internal app on the office network, not the public * internet. If this QR is ever exposed to customers, or the scan * target is ever made reachable without a login, the fix is to encode * an unguessable per-note token instead — e.g. * `DN26100401.`, * which needs no new column because it is recomputed rather than * stored. Raise it before the surface changes, not after. */ export type QrMatrix = { /** Modules per side, excluding the quiet zone. A version-1 code is 21. */ size: number; /** Row-major, length `size * size`. `true` = dark module. */ modules: boolean[]; /** * QR spec requires 4 modules of light margin on every side, and * scanners rely on it. Without it a code sitting against other ink * on the page fails to decode, so the padding is part of the * contract, not decoration. */ quietZone: number; }; /** * Encode `text` as a QR matrix. * * Error correction `M` (~15%) is the deliberate middle: enough to keep * the code readable through a thermal-grade print, a fingerprint smear * and a bent-in-a-pocket scan, without inflating the module count (and * so the physical size) the way `H` would for a payload this short. */ export function buildQrMatrix(text: string): QrMatrix { const trimmed = text.trim(); if (!trimmed) { throw new Error("buildQrMatrix: refusing to encode an empty string"); } const qr = QRCode.create(trimmed, { errorCorrectionLevel: "M" }); const size = qr.modules.size; // `qr.modules.data` is a FLAT row-major Uint8Array of size*size. // (It is not an array of rows — `data[r]` is a single module, which // is a trap worth naming.) const modules: boolean[] = new Array(size * size); for (let i = 0; i < modules.length; i++) { modules[i] = qr.modules.data[i] === 1; } return { size, modules, quietZone: 4 }; }