// Asset loading for the QSL card renderer: designer fonts (registered as // @font-face AND kept as CSS text so rasterization can inline them into the // serialized SVG — an SVG drawn through an cannot see document fonts), // photos with their natural dimensions, and the country flag. import { QSLFonts, QSLPhotoDataURL, QSLFlagDataURL } from '../../../wailsjs/go/main/App'; import type { CardTemplate, QSLFontInfo } from './qslTypes'; export interface PhotoAsset { url: string; // data URL w: number; // natural pixel size h: number; } export interface CardAssets { photos: Record; // keyed by template photo ref flagUrl: string; // data URL, '' when no flag fontCss: string; // @font-face rules with data: sources } // Weight each embedded family renders at (variable fonts need an explicit // heavy weight to match the printed-card look). export const FONT_WEIGHTS: Record = { 'Baloo 2': 800, Oswald: 700, }; let fontsPromise: Promise<{ css: string; fonts: QSLFontInfo[] }> | null = null; // loadFonts fetches the embedded fonts once, registers them with the // document and returns the @font-face CSS for SVG embedding. export function loadFonts(): Promise<{ css: string; fonts: QSLFontInfo[] }> { if (!fontsPromise) { fontsPromise = (async () => { const fonts = (await QSLFonts()) as QSLFontInfo[]; const rules = fonts.map((f) => { const weight = f.variable ? '100 900' : 'normal'; return `@font-face{font-family:'${f.family}';src:url(data:font/ttf;base64,${f.data_b64}) format('truetype');font-weight:${weight};}`; }); const css = rules.join('\n'); const el = document.createElement('style'); el.dataset.qslFonts = '1'; el.textContent = css; document.head.appendChild(el); await document.fonts.ready; return { css, fonts }; })(); } return fontsPromise; } async function imageSize(url: string): Promise<{ w: number; h: number }> { const img = new Image(); img.src = url; await img.decode(); return { w: img.naturalWidth, h: img.naturalHeight }; } // Photos are multi-MB base64 payloads; cache them so the three proposals // (which share the same photo set) and editor re-renders fetch each once. const photoCache = new Map>(); function loadPhoto(templateId: number, ref: string): Promise { const key = `${templateId}:${ref}`; let p = photoCache.get(key); if (!p) { p = (async () => { const url = await QSLPhotoDataURL(templateId, ref); const size = await imageSize(url); return { url, ...size }; })(); photoCache.set(key, p); } return p; } // loadCardAssets resolves everything a template needs to render: each // referenced photo as a data URL with its natural size, the flag and the // font CSS. templateId 0 reads draft photos at their original paths. export async function loadCardAssets(template: CardTemplate, templateId: number): Promise { const refs = new Set([template.hero.photo]); for (const e of template.elements) { if (e.type === 'insert' && e.photo) refs.add(e.photo); } const photos: Record = {}; await Promise.all( [...refs].map(async (ref) => { photos[ref] = await loadPhoto(templateId, ref); }), ); let flagUrl = ''; const country = template.elements.find((e) => e.type === 'country'); if (country?.flag && country.flag !== 'auto') { flagUrl = await QSLFlagDataURL(country.flag); } const { css } = await loadFonts(); return { photos, flagUrl, fontCss: css }; }