Add font import for Now, Gotham and Open Sauce One overrides; package fonts for Void

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.
This commit is contained in:
2026-10-04 13:58:06 +05:30
parent 4d18a50753
commit afbc0aa56f
37 changed files with 2436 additions and 72 deletions
+17
View File
@@ -1,4 +1,5 @@
import type { MarginPreset, PageId } from "../engine/geometry";
import { isUserFace, type UserFace } from "../fonts/userFontStore";
/** Page and template choices frozen with an invoice at issue (stored as JSON). */
export interface RenderPrefsV1 {
@@ -12,6 +13,11 @@ export interface RenderPrefsV1 {
/** Templates without a reference logo placeholder (Linea, ...) show the logo only when this is true. */
show?: boolean;
};
/**
* User-imported fonts in use, per face (Now, Gotham, Open Sauce One): the sha256 of each file (one per weight/style).
* Frozen with the invoice, which is what keeps those font files from being deleted. Absent: the bundled substitutes.
*/
fonts?: Partial<Record<UserFace, string[]>>;
}
export const DEFAULT_PREFS: RenderPrefsV1 = {
@@ -22,6 +28,7 @@ export const DEFAULT_PREFS: RenderPrefsV1 = {
};
const PAGE_SIZE_IDS: readonly PageId[] = ["A4", "LETTER"];
const FONT_ID = /^[0-9a-f]{16,64}$/;
const MARGIN_IDS: readonly MarginPreset[] = ["template", "narrow", "normal"];
/** Tolerant: anything unreadable or out of range falls back to the default for that field. */
@@ -49,6 +56,16 @@ export function parsePrefs(json: string | null | undefined): RenderPrefsV1 {
if (typeof show === "boolean") out.show = show;
if (Object.keys(out).length > 0) prefs.logo = out;
}
const fonts = o.fonts;
if (typeof fonts === "object" && fonts !== null) {
const out: NonNullable<RenderPrefsV1["fonts"]> = {};
for (const [face, ids] of Object.entries(fonts as Record<string, unknown>)) {
if (!isUserFace(face) || !Array.isArray(ids)) continue;
const valid = ids.filter((id): id is string => typeof id === "string" && FONT_ID.test(id)).slice(0, 16);
if (valid.length > 0) out[face] = valid;
}
if (Object.keys(out).length > 0) prefs.fonts = out;
}
return prefs;
}