/** * Compact SVG serialisation of a QR module matrix. * * Emits one move + horizontal-run command per dark stretch on each * row. Gzip likes the repetition; for a typical 25-30-module Thai QR * Payment payload the path is 1-2 KB before compression. */ import type { QRMatrix } from '@thai-qr-payment/qr'; /** Build a path-data string covering every dark module in the matrix. */ export function matrixToPath(matrix: QRMatrix): string { const segments: string[] = []; for (let y = 0; y < matrix.size; y += 1) { const row = matrix.modules[y]; if (!row) continue; let x = 0; while (x < row.length) { if (!row[x]) { x += 1; continue; } let runEnd = x; while (runEnd < row.length && row[runEnd]) runEnd += 1; const runLen = runEnd - x; // Single-row rectangle: M x y h v1 h- z segments.push(`M${x} ${y}h${runLen}v1h-${runLen}z`); x = runEnd; } } return segments.join(''); } export interface QRSvgOptions { /** Output width in user units (px by default). Defaults to module count. */ size?: number; /** Quiet-zone width in modules. EMVCo recommends 4. */ quietZone?: number; /** Dark module colour. Defaults to `#000`. */ foreground?: string; /** Background colour (set to `transparent` to omit). Defaults to `#fff`. */ background?: string; /** Additional attributes added verbatim to the root `` element. */ rootAttributes?: Record; } /** Render a QR matrix to a self-contained `` string. */ export function renderQRSvg(matrix: QRMatrix, options: QRSvgOptions = {}): string { const quietZone = toSafeUint(options.quietZone, 4); const fg = options.foreground ?? '#000'; const bg = options.background ?? '#fff'; const total = toSafeUint(matrix.size, 0) + quietZone * 2; const size = toSafeUint(options.size, total); const path = matrixToPath(matrix); const extra = options.rootAttributes ? ' ' + Object.entries(options.rootAttributes) .map(([k, v]) => `${k}="${escapeXmlAttribute(v)}"`) .join(' ') : ''; return ( `` + (bg === 'transparent' ? '' : ``) + `` + `` + `` ); } function toSafeUint(value: unknown, fallback: number): number { const n = Number(value); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback; } function escapeXmlAttribute(value: string): string { return value.replace(/[&<>"']/g, (c) => { switch (c) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; } return c; }); } export { escapeXmlAttribute };