qsl designer
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user