// Rasterizes the CardPreview SVG to bitmap bytes. The SVG is serialized and // loaded through an , so every resource it needs (fonts, photos, flag) // must be inline data URLs — CardPreview guarantees that. Fonts are awaited // before drawing so glyphs never rasterize with a fallback face. // rasterizeCard returns base64 image bytes (no data: prefix). // type 'image/jpeg' targets e-mail (maxBytes enforced by stepping quality // down); 'image/png' is used for template thumbnails. export async function rasterizeCard( svgEl: SVGSVGElement, outW: number, outH: number, type: 'image/jpeg' | 'image/png', maxBytes = 800 * 1024, ): Promise { await document.fonts.ready; const clone = svgEl.cloneNode(true) as SVGSVGElement; clone.setAttribute('width', String(outW)); clone.setAttribute('height', String(outH)); clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); const xml = new XMLSerializer().serializeToString(clone); const url = URL.createObjectURL(new Blob([xml], { type: 'image/svg+xml;charset=utf-8' })); try { const img = new Image(); img.src = url; await img.decode(); const canvas = document.createElement('canvas'); canvas.width = outW; canvas.height = outH; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('canvas 2d context unavailable'); if (type === 'image/jpeg') { ctx.fillStyle = '#ffffff'; // JPEG has no alpha — avoid black background ctx.fillRect(0, 0, outW, outH); } ctx.drawImage(img, 0, 0, outW, outH); if (type === 'image/png') { return canvas.toDataURL('image/png').split(',')[1]; } // Step the JPEG quality down until the e-mail size target is met. for (const q of [0.9, 0.8, 0.7, 0.6]) { const b64 = canvas.toDataURL('image/jpeg', q).split(',')[1]; if (b64.length * 0.75 <= maxBytes) return b64; } return canvas.toDataURL('image/jpeg', 0.5).split(',')[1]; } finally { URL.revokeObjectURL(url); } }