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.
158 lines
7.2 KiB
TypeScript
158 lines
7.2 KiB
TypeScript
import type { Sides } from "../../engine/geometry";
|
|
import { defineRoles, type TypeToken } from "../../fonts/roles";
|
|
import type { GapRange } from "../neutral/variant";
|
|
|
|
const L = "Lustria" as const;
|
|
const N = "Now" as const;
|
|
|
|
export const INK = "#000000";
|
|
/** Hairlines around the parties band. */
|
|
export const RULE = "#E7E1E6";
|
|
/** Zebra rows. */
|
|
export const ZEBRA = "#F7F7F8";
|
|
/** The flat/gradient stand-in for the marble photograph: a light warm grey with a soft sheen. */
|
|
export const BAND_FROM = "#F4F3F5";
|
|
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 reference widths (0.10 to 0.13 em on top of the design's
|
|
* 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, 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, 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, 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, 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, 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, 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, ownFont: { tracking: 0.04 }, minSize: 7 },
|
|
footer: { face: N, weight: 400, size: 7 },
|
|
});
|
|
|
|
export type MarbleRoles = typeof MARBLE_ROLES;
|
|
|
|
/**
|
|
* Reference A4 numbers: the rules and zebra bands start at x 31.5 and end at 549.8 (the text is inset 7 and 9.5 pt),
|
|
* the right-hand column ends at 549.0; y values are the design's distances turned into flow gaps.
|
|
*/
|
|
export interface MarbleVariant {
|
|
roles: MarbleRoles;
|
|
ink: string;
|
|
margins: Sides;
|
|
marginFloor: Sides;
|
|
/** Text inset inside the rules, left (parties) and right. */
|
|
inset: { left: number; right: number };
|
|
/** The top band's height and the bottom band's (A4 and Letter share the bottom band; the footer text is capped by lines). */
|
|
bands: { top: number; bottom: number };
|
|
head: {
|
|
/** Title line box below the header's top (the logo slot's top). */
|
|
titleTop: number;
|
|
/** Title box bottom to the first line of the stack. */
|
|
titleGap: number;
|
|
/** The widest share of the content width the right-hand stack may take. */
|
|
maxShare: number;
|
|
minStackW: number;
|
|
/** Vendor name top below the header's top when there is no logo; below the logo box otherwise. */
|
|
nameTop: number;
|
|
nameGap: number;
|
|
minNameW: number;
|
|
/** Space between the left column and the stack at its narrowest. */
|
|
colGap: number;
|
|
/** The logo slot's box height. */
|
|
logoH: number;
|
|
};
|
|
rule: number;
|
|
parties: { colA: number; colB: number; headingGap: number };
|
|
gaps: {
|
|
/** Header bottom to the first hairline. */
|
|
headRule: GapRange;
|
|
/** Hairline to the first heading, and the last info line to the second hairline. */
|
|
infoTop: GapRange;
|
|
infoBottom: GapRange;
|
|
/** Second hairline to the table heading. */
|
|
band: GapRange;
|
|
/** Last table row to the totals. */
|
|
tail: GapRange;
|
|
/** The totals (or the notes) to the signature band. */
|
|
close: GapRange;
|
|
};
|
|
table: {
|
|
/** Heading box bottom to the first zebra band. */
|
|
headGap: number;
|
|
descInset: number;
|
|
headDescInset: number;
|
|
cols: { qty: number; rate: number; amount: number };
|
|
numPad: number;
|
|
/** Right padding of the rate column (the unit is part of the rate, so it gets less than the amounts). */
|
|
ratePad: number;
|
|
padY: (rows: number) => number;
|
|
padYMin: number;
|
|
};
|
|
totals: { boxW: number; rowGap: number; dueGap: number; rightInset: number; leftGap: number; wordsMax: number };
|
|
closing: { signW: number; gap: number; notesBesideMax: number; signatureImageH: number };
|
|
footer: {
|
|
/** Footer text left edge, from the page's left edge (A4 reference: x 40.7) and the second column's left (x 197.8). */
|
|
col1: number;
|
|
col2: number;
|
|
colGap: number;
|
|
col1MaxW: number;
|
|
/** Band top to the first line box. */
|
|
top: number;
|
|
/** Lines the first column may take (name plus address) on A4 and Letter. */
|
|
maxLines: number;
|
|
};
|
|
}
|
|
|
|
export const MARBLE_VARIANT: MarbleVariant = {
|
|
roles: MARBLE_ROLES,
|
|
ink: INK,
|
|
margins: { top: 86, right: 45.8, bottom: 24, left: 31.5 },
|
|
marginFloor: { top: 86, right: 31.5, bottom: 24, left: 31.5 },
|
|
inset: { left: 7, right: 0.8 },
|
|
bands: { top: 64, bottom: 130 },
|
|
head: { titleTop: 14.85, titleGap: 3.7, maxShare: 0.7, minStackW: 170, nameTop: 26.8, nameGap: 6, minNameW: 90, colGap: 24, logoH: 50 },
|
|
rule: 0.75,
|
|
parties: { colA: 172.9, colB: 168.6, headingGap: 2.6 },
|
|
gaps: {
|
|
headRule: { nominal: 36.8, min: 14 },
|
|
infoTop: { nominal: 16.05, min: 10 },
|
|
infoBottom: { nominal: 11.56, min: 8 },
|
|
band: { nominal: 50.2, min: 22 },
|
|
tail: { nominal: 14, min: 6 },
|
|
close: { nominal: 14, min: 8 },
|
|
},
|
|
table: {
|
|
headGap: 8.85,
|
|
descInset: 9.5,
|
|
headDescInset: 7.3,
|
|
cols: { qty: 84, rate: 72.9, amount: 74.9 },
|
|
numPad: 9.5,
|
|
ratePad: 5,
|
|
padY: (rows) => (rows <= 8 ? 7.3 : rows <= 14 ? 6 : 4.5),
|
|
padYMin: 3.5,
|
|
},
|
|
totals: { boxW: 230, rowGap: 0, dueGap: 10, rightInset: 14.1, leftGap: 16, wordsMax: 280 },
|
|
closing: { signW: 170, gap: 24, notesBesideMax: 90, signatureImageH: 40 },
|
|
footer: { col1: 40.7, col2: 197.8, colGap: 24, col1MaxW: 190, top: 24.7, maxLines: 5 },
|
|
};
|
|
|
|
export const leading = (t: TypeToken) => t.leading as number;
|