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
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_PREFS } from "../pdf/model/prefs";
import { clientRefusal, selectionFrom, withUserFonts, type UserFontRow } from "./fontImport";
const row = (id: number, face: UserFontRow["face"], sha: string, over: Partial<UserFontRow> = {}): UserFontRow => ({
id, face, familyName: "F", fullName: "F", styleName: "", weight: 400, style: "normal", sha256: sha, fileName: "f.ttf",
format: "ttf", size: 1, fsType: 0, hasRupee: true, licenceAckAt: "t", importedAt: "t", hidden: false, ...over,
});
const A = "a".repeat(64);
const B = "b".repeat(64);
const C = "c".repeat(64);
describe("clientRefusal", () => {
const bytes = (magic: string) => new Uint8Array([...magic].map((c) => c.charCodeAt(0)).concat(new Array(20).fill(0)));
it("passes TTF, OTF and WOFF and refuses the rest with a reason", () => {
expect(clientRefusal(new Uint8Array([0, 1, 0, 0, 0, 0, 0, 0]))).toBeNull();
expect(clientRefusal(bytes("OTTO"))).toBeNull();
expect(clientRefusal(bytes("wOFF"))).toBeNull();
expect(clientRefusal(bytes("wOF2"))).toMatch(/WOFF2/);
expect(clientRefusal(bytes("ttcf"))).toMatch(/collections/);
expect(clientRefusal(bytes("<htm"))).toMatch(/not a TrueType/);
});
});
describe("withUserFonts", () => {
const fonts = [row(1, "Gotham", B), row(2, "Gotham", A, { weight: 700 }), row(3, "Now", C), row(4, "Now", "d".repeat(64), { hidden: true })];
it("lists visible fonts per face", () => {
expect(selectionFrom(fonts)).toEqual({ Gotham: [B, A], Now: [C] });
});
it("adds only the faces the template draws, sorted, and nothing for a template without them", () => {
expect(withUserFonts({ ...DEFAULT_PREFS, templateId: "purple-pop" }, fonts).fonts).toEqual({ Gotham: [A, B] });
expect(withUserFonts({ ...DEFAULT_PREFS, templateId: "citrus-split" }, fonts).fonts).toEqual({ Now: [C], Gotham: [A, B] });
expect(withUserFonts({ ...DEFAULT_PREFS, templateId: "marble" }, fonts).fonts).toEqual({ Now: [C] });
expect(withUserFonts({ ...DEFAULT_PREFS, templateId: "classic" }, fonts)).toEqual({ ...DEFAULT_PREFS, templateId: "classic" });
expect(withUserFonts({ ...DEFAULT_PREFS, templateId: "marble" }, [])).toEqual({ ...DEFAULT_PREFS, templateId: "marble" });
});
it("replaces fonts already in the prefs rather than merging", () => {
const stale = { ...DEFAULT_PREFS, templateId: "marble", fonts: { Now: ["e".repeat(64)] } };
expect(withUserFonts(stale, fonts).fonts).toEqual({ Now: [C] });
expect(withUserFonts(stale, []).fonts).toBeUndefined();
});
});