/** * Country flags for the price-sheet PDF. * * Source = the same SVGs the price-sheet matrix shows (`public/flags/{ISO2}.svg`), * bundled into the server build as the Nitro server asset `assets:flags` * (nuxt.config → nitro.serverAssets). pdfmake accepts only PNG/JPEG images and * its SVG support chokes on the Wikimedia-style flags (clipPath / use / coats of * arms), so every flag is rasterised ONCE per process with sharp (fixed height, * natural aspect ratio, 1 px light border so white flags stay visible on paper) * and cached as a PNG data URL. Missing / broken flags resolve to null and the * PDF simply prints the country name without a flag. */ const FLAG_RASTER_HEIGHT_PX = 36 const FLAG_BORDER = '#c8ced6' /** Widest flag we allow in the PDF (pt) at FLAG_PDF_HEIGHT — Qatar/Switzerland-style ratios are clamped. */ export const FLAG_PDF_HEIGHT = 10 export const FLAG_PDF_MAX_WIDTH = 18 export interface FlagImage { dataUrl: string /** pdfmake width in pt at FLAG_PDF_HEIGHT (aspect ratio preserved, capped). */ width: number height: number } const cache = new Map>() const render = async (code: string): Promise => { try { const storage = useStorage('assets:flags') const raw: any = await storage.getItemRaw(`${code}.svg`) if (!raw) return null const svg = Buffer.isBuffer(raw) ? raw : Buffer.from(raw) const sharp = (await import('sharp')).default // No density: sharp re-renders an SVG at the resize scale anyway. Some flags declare a huge // intrinsic size (BB) and trip the default input-pixel guard → limitInputPixels off. const png = await sharp(svg, { limitInputPixels: false }) .resize({ height: FLAG_RASTER_HEIGHT_PX }) .flatten({ background: '#ffffff' }) .extend({ top: 1, bottom: 1, left: 1, right: 1, background: FLAG_BORDER }) .png({ compressionLevel: 9 }) .toBuffer({ resolveWithObject: true }) const ratio = png.info.width / Math.max(1, png.info.height) const width = Math.min(FLAG_PDF_MAX_WIDTH, Math.round(FLAG_PDF_HEIGHT * ratio * 10) / 10) return { dataUrl: `data:image/png;base64,${png.data.toString('base64')}`, width, height: FLAG_PDF_HEIGHT } } catch (err: any) { console.warn('[PriceSheet] flag render failed for', code, err?.message || err) return null } } export const getFlagImage = (countryCode: string | null | undefined): Promise => { const code = String(countryCode || '').trim().toUpperCase() if (!/^[A-Z]{2}$/.test(code)) return Promise.resolve(null) if (!cache.has(code)) cache.set(code, render(code)) return cache.get(code)! } /** Flags for a set of ISO2 codes — only the ones that rendered. */ export const loadFlagImages = async (codes: Iterable): Promise> => { const unique = [...new Set([...codes].map(c => String(c || '').trim().toUpperCase()).filter(c => /^[A-Z]{2}$/.test(c)))] const out: Record = {} const rendered = await Promise.all(unique.map(async code => [code, await getFlagImage(code)] as const)) for (const [code, img] of rendered) if (img) out[code] = img return out }