Files
OpsLog/frontend/src/components/qsl/rasterize.ts
T
2026-06-11 21:54:35 +02:00

53 lines
2.0 KiB
TypeScript

// Rasterizes the CardPreview SVG to bitmap bytes. The SVG is serialized and
// loaded through an <img>, 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<string> {
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);
}
}