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
+26
View File
@@ -2,6 +2,7 @@ import { create, type Font } from "fontkit";
import { FONT_MANIFEST } from "../fonts/manifest";
import { familySources, FONT_STACK, stack, type FamilySource } from "../fonts/stack";
import { resolveFace, roleToken, type RoleName, type TypeToken } from "../fonts/roles";
import { userFamilySources } from "../fonts/userFontStore";
export interface MeasureInput {
text: string;
@@ -29,6 +30,29 @@ export interface Measurer {
missing(text: string, face?: string): number[];
}
const userFonts = new Map<string, Font>();
/**
* The fontkit font of a user-imported family at a weight and style (the file react-pdf resolves for it), or
* undefined when `familyName` is not a user family. Parsed once per file and shared by every measurer.
*/
export function userFontkitFont(familyName: string, weight: number, italic: boolean): Font | null | undefined {
const sources = userFamilySources(familyName);
if (sources.length === 0) return undefined;
const src = resolveSource(
sources.map((s) => ({ file: s.id, fontWeight: s.fontWeight, fontStyle: s.fontStyle })),
weight,
italic ? "italic" : "normal",
);
if (!src) return null;
let font = userFonts.get(src.file);
if (!font) {
font = create((sources.find((s) => s.id === src.file) as { bytes: Uint8Array }).bytes) as Font;
userFonts.set(src.file, font);
}
return font;
}
/** react-pdf's FontFamily.resolve, so the measurer reads the same file the renderer embeds. */
function resolveSource(sources: FamilySource[], weight: number, style: "normal" | "italic"): FamilySource | undefined {
const styled = sources.filter((s) => s.fontStyle === style);
@@ -57,6 +81,8 @@ export function createMeasurer(loadBytes: (file: string) => Uint8Array): Measure
const fonts = new Map<string, Font>();
function load(familyName: string, weight: number, italic: boolean): Font | null {
const own = userFontkitFont(familyName, weight, italic);
if (own !== undefined) return own;
const fam = FONT_MANIFEST.families.find((f) => f.family === familyName);
if (!fam) return null;
const src = resolveSource(familySources(fam), weight, italic ? "italic" : "normal");