Font import: - Migration M6 adds user_fonts; files are stored content-addressed under the local data dir. New Rust commands inspect, import, list, read and remove fonts, refusing WOFF2, collections, variable fonts and restricted fsType, and unpacking WOFF1 to plain sfnt. - Imported fonts register in the render worker as user:<Face> families with an italic alias, and are pinned in the frozen render prefs so issued invoices can still re-render. Fonts referenced by issued invoices are hidden, never deleted. - Size and tracking compensations tuned for the substitutes apply only while the face is not overridden; default output and fingerprint goldens are unchanged. - Settings gets a Fonts section with a licence acknowledgement. Void packaging: - Install fonts to /usr/share/fonts/voiced with per-family OFL licences, set font_dirs, and add INSTALL/REMOVE scripts that run fc-cache. xbps-create carries both scripts (verified in a void-glibc container). - Fix xbps-create dependency specs in build-xbps.sh.
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
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<UserFace, string> = {
|
|
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<number, string> = { 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<RenderPrefsV1["fonts"]> {
|
|
const out: NonNullable<RenderPrefsV1["fonts"]> = {};
|
|
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<RenderPrefsV1["fonts"]> = {};
|
|
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());
|