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 d955130cf3
commit 43afdaf4bf
37 changed files with 2436 additions and 72 deletions
+8 -3
View File
@@ -11,6 +11,11 @@ export interface TemplateCatalogEntry {
badge?: "Matches your original format";
/** No Canva placeholder: the logo shows only when the user turns it on. */
logoOptIn?: boolean;
/**
* The licensed faces this template draws (a user-imported font may replace them). Mirrors the roles of the
* template; a test keeps the two in step.
*/
faces?: readonly ("Now" | "Gotham" | "Open Sauce One")[];
}
export const FAMILY_LABELS: Record<TemplateCatalogEntry["family"], string> = {
@@ -32,11 +37,11 @@ export const TEMPLATE_CATALOG: readonly TemplateCatalogEntry[] = [
{ id: "tangerine-ledger", name: "Tangerine Ledger", family: "ledger", thumbnail: "/templates/tangerine-ledger.png" },
{ id: "slate-band", name: "Slate Band", family: "band", thumbnail: "/templates/slate-band.png" },
{ id: "teal-swoosh", name: "Teal Swoosh", family: "band", thumbnail: "/templates/teal-swoosh.png" },
{ id: "purple-pop", name: "Purple Pop", family: "northline", thumbnail: "/templates/purple-pop.png", logoOptIn: true },
{ id: "citrus-split", name: "Citrus Split", family: "northline", thumbnail: "/templates/citrus-split.png" },
{ id: "purple-pop", name: "Purple Pop", family: "northline", thumbnail: "/templates/purple-pop.png", logoOptIn: true, faces: ["Gotham", "Open Sauce One"] },
{ id: "citrus-split", name: "Citrus Split", family: "northline", thumbnail: "/templates/citrus-split.png", faces: ["Now", "Gotham"] },
{ id: "monolith", name: "Monolith", family: "standalone", thumbnail: "/templates/monolith.png" },
{ id: "cobalt-stripe", name: "Cobalt Stripe", family: "standalone", thumbnail: "/templates/cobalt-stripe.png" },
{ id: "marble", name: "Marble", family: "standalone", thumbnail: "/templates/marble.png", logoOptIn: true },
{ id: "marble", name: "Marble", family: "standalone", thumbnail: "/templates/marble.png", logoOptIn: true, faces: ["Now"] },
{ id: "highlighter", name: "Highlighter", family: "standalone", thumbnail: "/templates/highlighter.png", logoOptIn: true },
];
+12 -11
View File
@@ -18,31 +18,32 @@ export const BAND_TO = "#ECEAEE";
* Marble's type (docs/templates/01-marble.md): Lustria in tracked caps for the title, headings, table heads and the
* amount due; "Now" (drawn in Jost, the bundled look-alike) for everything else. Jost sets about 12% narrower than Now,
* so every Now role carries the tracking that restores the Canva widths (0.10 to 0.13 em on top of the design's
* 0.04 em). Lustria ascent 0.955, descent 0.327; Jost 1.07 / 0.375.
* 0.04 em). A real Now imported by the user replaces Jost, so those roles take the design's 0.04 em back through
* ownFont. Lustria ascent 0.955, descent 0.327; Jost 1.07 / 0.375.
*/
export const MARBLE_ROLES = defineRoles({
/** "INVOICE" / "TAX INVOICE": 25.18 pt, tracked 0.192 em, shrunk to the room the stack has. */
title: { face: L, weight: 400, size: 25.18, leading: 25.2, tracking: 0.192, caps: true, minSize: 13 },
/** The vendor's name at the header's left (the design leaves it empty). */
brand: { face: L, weight: 400, size: 12.65, leading: 18.2, tracking: 0.11, caps: true, minSize: 9 },
meta: { face: N, weight: 400, size: 11.55, leading: 17.5, tracking: 0.105, minSize: 8 },
meta: { face: N, weight: 400, size: 11.55, leading: 17.5, tracking: 0.105, ownFont: { tracking: 0.04 }, minSize: 8 },
/** BILL TO:, PAYABLE TO:, SUPPLIER:. */
heading: { face: L, weight: 400, size: 12.65, leading: 18.2, tracking: 0.11, caps: true },
body: { face: N, weight: 400, size: 11.55, leading: 18.2, tracking: 0.105, minSize: 8 },
body: { face: N, weight: 400, size: 11.55, leading: 18.2, tracking: 0.105, ownFont: { tracking: 0.04 }, minSize: 8 },
tableHead: { face: L, weight: 400, size: 11.68, leading: 16, tracking: 0.192, caps: true },
tableBody: { face: N, weight: 400, size: 10.57, leading: 17.4, tracking: 0.102 },
tableSub: { face: N, weight: 400, size: 8.5, leading: 11.5, tracking: 0.1 },
tableBody: { face: N, weight: 400, size: 10.57, leading: 17.4, tracking: 0.102, ownFont: { tracking: 0.04 } },
tableSub: { face: N, weight: 400, size: 8.5, leading: 11.5, tracking: 0.1, ownFont: { tracking: 0.04 } },
totalLabel: { face: L, weight: 400, size: 9, leading: 16, tracking: 0.192, caps: true },
totalValue: { face: N, weight: 400, size: 10.57, leading: 16, tracking: 0.102 },
totalValue: { face: N, weight: 400, size: 10.57, leading: 16, tracking: 0.102, ownFont: { tracking: 0.04 } },
/** AMOUNT DUE: 14.6 pt, tracked 0.192 em. */
due: { face: L, weight: 400, size: 14.6, leading: 20, tracking: 0.192, caps: true, minSize: 10 },
small: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13 },
small: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13, ownFont: { tracking: 0.04 } },
smallLabel: { face: L, weight: 400, size: 9, leading: 15.3, tracking: 0.192, caps: true },
signName: { face: N, weight: 400, size: 10.57, leading: 15, tracking: 0.102 },
signDesignation: { face: N, weight: 400, size: 9.6, leading: 14, tracking: 0.13 },
fine: { face: N, weight: 400, size: 8, leading: 11, tracking: 0.1 },
signName: { face: N, weight: 400, size: 10.57, leading: 15, tracking: 0.102, ownFont: { tracking: 0.04 } },
signDesignation: { face: N, weight: 400, size: 9.6, leading: 14, tracking: 0.13, ownFont: { tracking: 0.04 } },
fine: { face: N, weight: 400, size: 8, leading: 11, tracking: 0.1, ownFont: { tracking: 0.04 } },
/** The footer band's contact lines. */
contact: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13, minSize: 7 },
contact: { face: N, weight: 400, size: 9.6, leading: 15.3, tracking: 0.13, ownFont: { tracking: 0.04 }, minSize: 7 },
footer: { face: N, weight: 400, size: 7 },
});
+11 -11
View File
@@ -28,21 +28,21 @@ export const CITRUS_ROLES = defineRoles({
metaStrong: { face: DM, weight: 700, size: 12.36, leading: 16, minSize: 8 },
billedLabel: { face: DM, weight: 700, size: 12.4, leading: 17, caps: true },
clientName: { face: DM, weight: 700, size: 14.87, leading: 19, caps: true },
clientLine: { face: G, weight: 400, size: gotham(10.5), leading: 15.5 },
headService: { face: G, weight: 700, size: gotham(15.17), leading: 18, tracking: 0.15, caps: true },
headNum: { face: G, weight: 700, size: gotham(14.17), leading: 18, caps: true },
tableBody: { face: G, weight: 400, size: gotham(12), leading: 15 },
clientLine: { face: G, weight: 400, size: gotham(10.5), ownFont: { size: 10.5 }, leading: 15.5 },
headService: { face: G, weight: 700, size: gotham(15.17), ownFont: { size: 15.17 }, leading: 18, tracking: 0.15, caps: true },
headNum: { face: G, weight: 700, size: gotham(14.17), ownFont: { size: 14.17 }, leading: 18, caps: true },
tableBody: { face: G, weight: 400, size: gotham(12), ownFont: { size: 12 }, leading: 15 },
tableSub: { face: G, weight: 400, size: 9, leading: 11.5 },
totalLabel: { face: G, weight: 700, size: gotham(12.5), leading: 17, caps: true },
totalValue: { face: G, weight: 400, size: gotham(12.5), leading: 17 },
grand: { face: G, weight: 700, size: gotham(14.7), leading: 18, caps: true },
grandValue: { face: G, weight: 700, size: gotham(14.7), leading: 18 },
totalLabel: { face: G, weight: 700, size: gotham(12.5), ownFont: { size: 12.5 }, leading: 17, caps: true },
totalValue: { face: G, weight: 400, size: gotham(12.5), ownFont: { size: 12.5 }, leading: 17 },
grand: { face: G, weight: 700, size: gotham(14.7), ownFont: { size: 14.7 }, leading: 18, caps: true },
grandValue: { face: G, weight: 700, size: gotham(14.7), ownFont: { size: 14.7 }, leading: 18 },
words: { face: DM, weight: 700, size: 11, leading: 14 },
wordsLabel: { face: DM, weight: 700, size: 11, leading: 14 },
sectionLabel: { face: DM, weight: 700, size: 12, leading: 16, caps: true },
bank: { face: G, weight: 400, size: gotham(11), leading: 15 },
bullet: { face: G, weight: 400, size: gotham(12), leading: 16.5 },
contact: { face: G, weight: 400, size: gotham(12.77), leading: 18, minSize: 7 },
bank: { face: G, weight: 400, size: gotham(11), ownFont: { size: 11 }, leading: 15 },
bullet: { face: G, weight: 400, size: gotham(12), ownFont: { size: 12 }, leading: 16.5 },
contact: { face: G, weight: 400, size: gotham(12.77), ownFont: { size: 12.77 }, leading: 18, minSize: 7 },
small: { face: DM, weight: 400, size: 9, leading: 12 },
fine: { face: DM, weight: 400, size: 7, leading: 10 },
signName: { face: DM, weight: 700, size: 9.5, leading: 13 },
+10 -10
View File
@@ -22,25 +22,25 @@ export const PURPLE_ROLES = defineRoles({
wordmark: { face: "Open Sauce One", weight: 800, size: 94.07, leading: 84, tracking: -0.08, minSize: 48 },
taxCaption: { face: DM, weight: 700, size: 14, leading: 18, tracking: 0.15, caps: true },
brand: { face: DM, weight: 700, size: 17.77, leading: 21, minSize: 12 },
pill: { face: G, weight: 700, size: gotham(13.39), leading: 17, tracking: 0.13, caps: true, minSize: 8.5 },
label: { face: G, weight: 700, size: gotham(12), leading: 16, caps: true },
clientName: { face: G, weight: 700, size: gotham(14.39), leading: 18, caps: true },
clientLine: { face: G, weight: 400, size: gotham(10.16), leading: 15.5 },
pill: { face: G, weight: 700, size: gotham(13.39), ownFont: { size: 13.39 }, leading: 17, tracking: 0.13, caps: true, minSize: 8.5 },
label: { face: G, weight: 700, size: gotham(12), ownFont: { size: 12 }, leading: 16, caps: true },
clientName: { face: G, weight: 700, size: gotham(14.39), ownFont: { size: 14.39 }, leading: 18, caps: true },
clientLine: { face: G, weight: 400, size: gotham(10.16), ownFont: { size: 10.16 }, leading: 15.5 },
amount: { face: DM, weight: 700, size: 32.74, leading: 36, minSize: 20, tnum: true },
date: { face: DM, weight: 700, size: 14.05, leading: 19.5, caps: true, minSize: 10 },
tableHead: { face: DM, weight: 700, size: 14, leading: 18, caps: true },
tableBody: { face: DM, weight: 400, size: 11.83, leading: 15 },
tableSub: { face: DM, weight: 400, size: 9, leading: 11.5 },
totalLabel: { face: G, weight: 700, size: gotham(13), leading: 17, caps: true },
totalValue: { face: G, weight: 400, size: gotham(13), leading: 17 },
grand: { face: G, weight: 700, size: gotham(13), leading: 17, caps: true },
grandValue: { face: G, weight: 700, size: gotham(13), leading: 17 },
totalLabel: { face: G, weight: 700, size: gotham(13), ownFont: { size: 13 }, leading: 17, caps: true },
totalValue: { face: G, weight: 400, size: gotham(13), ownFont: { size: 13 }, leading: 17 },
grand: { face: G, weight: 700, size: gotham(13), ownFont: { size: 13 }, leading: 17, caps: true },
grandValue: { face: G, weight: 700, size: gotham(13), ownFont: { size: 13 }, leading: 17 },
words: { face: DM, weight: 400, size: 10, leading: 13 },
wordsLabel: { face: DM, weight: 700, size: 10, leading: 13 },
termsLabel: { face: DM, weight: 700, size: 13, leading: 17, caps: true },
bullet: { face: DM, weight: 400, size: 11, leading: 14.2 },
bank: { face: G, weight: 400, size: gotham(11), leading: 12.5 },
contact: { face: G, weight: 400, size: gotham(10.14), leading: 15, minSize: 7 },
bank: { face: G, weight: 400, size: gotham(11), ownFont: { size: 11 }, leading: 12.5 },
contact: { face: G, weight: 400, size: gotham(10.14), ownFont: { size: 10.14 }, leading: 15, minSize: 7 },
small: { face: DM, weight: 400, size: 9, leading: 12 },
fine: { face: DM, weight: 400, size: 7, leading: 10 },
signName: { face: DM, weight: 700, size: 9.5, leading: 13 },
+1 -1
View File
@@ -5,7 +5,7 @@ import type { GapRange } from "../neutral/variant";
/**
* Gotham (commercial) is drawn with Montserrat, which runs about 4% wider at the same size. Every Gotham-derived
* size is multiplied by this when a token is defined, so the substitute takes the width the design has. A real
* Gotham imported over the licensed name would need these tokens at factor 1.
* Gotham imported over the licensed name takes the design size back: each such token carries it as `ownFont.size`.
*/
export const GOTHAM_SIZE = 0.96;
const round2 = (n: number) => Math.round(n * 100) / 100;