99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
// 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 <img> 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<string, PhotoAsset>; // 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<string, number> = {
|
|
'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<string, Promise<PhotoAsset>>();
|
|
|
|
function loadPhoto(templateId: number, ref: string): Promise<PhotoAsset> {
|
|
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<CardAssets> {
|
|
const refs = new Set<string>([template.hero.photo]);
|
|
for (const e of template.elements) {
|
|
if (e.type === 'insert' && e.photo) refs.add(e.photo);
|
|
}
|
|
const photos: Record<string, PhotoAsset> = {};
|
|
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 };
|
|
}
|