import type { LogoInput } from "../pdf/model/build"; import { imageSizeFromDataUri } from "./imageSize"; import type { Settings } from "./types"; export type LogoKind = "wordmark" | "mark" | "tall"; export type LogoIncludesName = "auto" | "yes" | "no"; /** The metrics the Rust logo pipeline stores as JSON in `settings.logoMeta`. */ export interface LogoMeta { width: number; height: number; aspect: number; inkDensity: number; meanColor: [number, number, number]; kind: LogoKind; hasAlpha: boolean; opaqueBackground: boolean; sourceWidth: number; sourceHeight: number; } /** What the backend returns for an imported logo (paths are relative to the data dir). */ export interface LogoAsset { originalPath: string; printPath: string; knockoutPath: string | null; metrics: LogoMeta; } export type LogoSettings = Pick; /** The loaded print image and, when the logo has transparency, its white knockout. */ export interface LogoImages { printUri: string; knockoutUri: string | null; } /** A logo with no readable size is fitted as if it filled Classic's 170 x 54 box. */ const UNKNOWN_LOGO_SIZE = { width: 170, height: 54 }; /** Ink density assumed for a logo that never went through the pipeline. */ const UNKNOWN_INK_DENSITY = 0.5; const KINDS: readonly string[] = ["wordmark", "mark", "tall"]; const isPositive = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n > 0; export function parseLogoMeta(json: string | null | undefined): LogoMeta | null { if (!json) return null; try { const m = JSON.parse(json) as Partial | null; if (!m || typeof m !== "object") return null; if (!isPositive(m.width) || !isPositive(m.height) || !isPositive(m.aspect)) return null; if (typeof m.inkDensity !== "number" || !Number.isFinite(m.inkDensity)) return null; if (typeof m.kind !== "string" || !KINDS.includes(m.kind)) return null; return { width: m.width, height: m.height, aspect: m.aspect, inkDensity: m.inkDensity, meanColor: Array.isArray(m.meanColor) && m.meanColor.length === 3 ? (m.meanColor as [number, number, number]) : [0, 0, 0], kind: m.kind as LogoKind, hasAlpha: Boolean(m.hasAlpha), opaqueBackground: Boolean(m.opaqueBackground), sourceWidth: Number(m.sourceWidth) || m.width, sourceHeight: Number(m.sourceHeight) || m.height, }; } catch { return null; } } /** "Automatic" means a wordmark already carries the business name. */ export function logoIncludesName(setting: string | undefined, kind: LogoKind | null): boolean { if (setting === "yes") return true; if (setting === "no") return false; return kind === "wordmark"; } /** The render-model logo from the loaded images and the stored metrics, or from the image itself for a legacy logo. */ export function logoForModel(vendor: LogoSettings, images: LogoImages | null): LogoInput | null { if (!images) return null; const meta = parseLogoMeta(vendor.logoMeta); const knockoutDataUri = meta?.hasAlpha ? images.knockoutUri : null; const hasKnockout = Boolean(knockoutDataUri); if (meta) { return { dataUri: images.printUri, width: meta.width, height: meta.height, aspect: meta.aspect, density: meta.inkDensity, kind: meta.kind, includesName: logoIncludesName(vendor.logoIncludesName, meta.kind), hasKnockout, ...(hasKnockout ? { knockoutDataUri: knockoutDataUri as string } : {}), }; } const size = imageSizeFromDataUri(images.printUri) ?? UNKNOWN_LOGO_SIZE; const aspect = size.width / size.height; const kind: LogoKind = aspect >= 2.5 ? "wordmark" : aspect <= 0.6 ? "tall" : "mark"; return { dataUri: images.printUri, ...size, aspect, density: UNKNOWN_INK_DENSITY, kind, includesName: logoIncludesName(vendor.logoIncludesName, kind), hasKnockout: false, }; } /** Loads the print and knockout images a vendor's logo settings point at. A missing knockout is not an error. */ export async function loadLogoImages( vendor: Pick, read: (path: string) => Promise, ): Promise { if (!vendor.logoPath) return null; const printUri = await read(vendor.logoPath); const knockoutUri = vendor.logoKnockoutPath ? await read(vendor.logoKnockoutPath).catch(() => null) : null; return { printUri, knockoutUri }; } /** "Wordmark • 4.6:1 • transparent background" */ export function logoFacts(meta: LogoMeta): string { const kind = { wordmark: "Wordmark", mark: "Mark", tall: "Tall logo" }[meta.kind]; const ratio = meta.aspect >= 1 ? `${meta.aspect.toFixed(1)}:1` : `1:${(1 / meta.aspect).toFixed(1)}`; return [kind, ratio, meta.hasAlpha ? "transparent background" : "solid background"].join(" • "); }