import type { FontFormat, UserFace } from "../pdf/fonts/userFontStore"; import { sniffFontFormat, USER_FACES } from "../pdf/fonts/userFontStore"; import type { RenderPrefsV1 } from "../pdf/model/prefs"; import { catalogEntry } from "../pdf/templates/catalog"; export { USER_FACES, type UserFace }; /** The three licensed faces and the bundled look-alike each one is drawn with until a font is imported. */ export const FACE_SUBSTITUTES: Record = { Now: "Jost", Gotham: "Montserrat", "Open Sauce One": "Poppins", }; /** What the backend found in a font file (src-tauri/src/commands/fonts.rs `FontInspection`). */ export interface FontInspection { format: "ttf" | "otf"; sourceFormat: "ttf" | "otf" | "woff"; family: string; subfamily: string; fullName: string; postscriptName: string; weight: number; italic: boolean; fsType: number; numGlyphs: number; hasRupee: boolean; hasBasicLatin: boolean; size: number; sha256: string; warnings: string[]; } /** One stored font file (`UserFont`). */ export interface UserFontRow { id: number; face: UserFace; familyName: string; fullName: string; styleName: string; weight: number; style: "normal" | "italic"; sha256: string; fileName: string; format: "ttf" | "otf"; size: number; fsType: number; hasRupee: boolean; licenceAckAt: string; importedAt: string; hidden: boolean; } export interface RemoveOutcome { deleted: boolean; hidden: boolean; } export interface ImportMeta { face: UserFace; fileName: string; weight?: number; italic?: boolean; licenceAcknowledged: boolean; } /** * The same magic-byte check the backend makes, to refuse early with a plain message. TTF, OTF and WOFF pass (the * backend unpacks WOFF); the backend stays the authority and repeats every check. */ export function clientRefusal(bytes: Uint8Array): string | null { const format: FontFormat = sniffFontFormat(bytes); switch (format) { case "ttf": case "otf": case "woff": return null; case "woff2": return "WOFF2 cannot be used: it embeds in a PDF without glyph outlines (invisible text). Import the TTF, OTF or WOFF file instead."; case "ttc": return "Font collections (.ttc) are not supported. Import a single TTF or OTF file."; default: return "This is not a TrueType, OpenType or WOFF font file."; } } export const weightName = (weight: number, italic: boolean): string => { const names: Record = { 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular", 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black" }; return `${names[weight] ?? weight}${italic ? " Italic" : ""}`; }; /** Visible fonts grouped by face, as the sha256 list `prefs.fonts` carries. */ export function selectionFrom(fonts: readonly UserFontRow[]): NonNullable { const out: NonNullable = {}; for (const f of fonts) { if (f.hidden) continue; (out[f.face] ??= []).push(f.sha256); } return out; } /** * The prefs to render and issue with: `prefs` plus the imported fonts for the faces this template draws. Fonts for * faces it does not use stay out, so an invoice only pins (and so only protects) the fonts it really uses. The * result is for rendering; keep the clean `prefs` for the page-setup controls and the saved default. */ export function withUserFonts(prefs: RenderPrefsV1, fonts: readonly UserFontRow[]): RenderPrefsV1 { const { fonts: _drop, ...rest } = prefs; const faces = catalogEntry(prefs.templateId).faces ?? []; const selection = selectionFrom(fonts); const used: NonNullable = {}; for (const face of faces) { const ids = selection[face]; if (ids && ids.length > 0) used[face] = [...ids].sort(); } return Object.keys(used).length > 0 ? { ...rest, fonts: used } : rest; } // Settings and the invoice view are separate parts of the UI; this lets the list in one refresh the other. const listeners = new Set<() => void>(); export const onUserFontsChanged = (cb: () => void): (() => void) => { listeners.add(cb); return () => listeners.delete(cb); }; export const notifyUserFontsChanged = (): void => listeners.forEach((cb) => cb());