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:
@@ -0,0 +1,125 @@
|
||||
import type { FontFormat, UserFace } from "../pdf/fonts/userFontStore";
|
||||
import { sniffFontFormat, USER_FACES } from "../pdf/fonts/userFontStore";
|
||||
import type { RenderPrefsV1 } from "../pdf/model/prefs";
|
||||
import { catalogEntry } from "../pdf/templates/catalog";
|
||||
|
||||
export { USER_FACES, type UserFace };
|
||||
|
||||
/** The three licensed faces and the bundled look-alike each one is drawn with until a font is imported. */
|
||||
export const FACE_SUBSTITUTES: Record<UserFace, string> = {
|
||||
Now: "Jost",
|
||||
Gotham: "Montserrat",
|
||||
"Open Sauce One": "Poppins",
|
||||
};
|
||||
|
||||
/** What the backend found in a font file (src-tauri/src/commands/fonts.rs `FontInspection`). */
|
||||
export interface FontInspection {
|
||||
format: "ttf" | "otf";
|
||||
sourceFormat: "ttf" | "otf" | "woff";
|
||||
family: string;
|
||||
subfamily: string;
|
||||
fullName: string;
|
||||
postscriptName: string;
|
||||
weight: number;
|
||||
italic: boolean;
|
||||
fsType: number;
|
||||
numGlyphs: number;
|
||||
hasRupee: boolean;
|
||||
hasBasicLatin: boolean;
|
||||
size: number;
|
||||
sha256: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/** One stored font file (`UserFont`). */
|
||||
export interface UserFontRow {
|
||||
id: number;
|
||||
face: UserFace;
|
||||
familyName: string;
|
||||
fullName: string;
|
||||
styleName: string;
|
||||
weight: number;
|
||||
style: "normal" | "italic";
|
||||
sha256: string;
|
||||
fileName: string;
|
||||
format: "ttf" | "otf";
|
||||
size: number;
|
||||
fsType: number;
|
||||
hasRupee: boolean;
|
||||
licenceAckAt: string;
|
||||
importedAt: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface RemoveOutcome {
|
||||
deleted: boolean;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface ImportMeta {
|
||||
face: UserFace;
|
||||
fileName: string;
|
||||
weight?: number;
|
||||
italic?: boolean;
|
||||
licenceAcknowledged: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The same magic-byte check the backend makes, to refuse early with a plain message. TTF, OTF and WOFF pass (the
|
||||
* backend unpacks WOFF); the backend stays the authority and repeats every check.
|
||||
*/
|
||||
export function clientRefusal(bytes: Uint8Array): string | null {
|
||||
const format: FontFormat = sniffFontFormat(bytes);
|
||||
switch (format) {
|
||||
case "ttf":
|
||||
case "otf":
|
||||
case "woff":
|
||||
return null;
|
||||
case "woff2":
|
||||
return "WOFF2 cannot be used: it embeds in a PDF without glyph outlines (invisible text). Import the TTF, OTF or WOFF file instead.";
|
||||
case "ttc":
|
||||
return "Font collections (.ttc) are not supported. Import a single TTF or OTF file.";
|
||||
default:
|
||||
return "This is not a TrueType, OpenType or WOFF font file.";
|
||||
}
|
||||
}
|
||||
|
||||
export const weightName = (weight: number, italic: boolean): string => {
|
||||
const names: Record<number, string> = { 100: "Thin", 200: "ExtraLight", 300: "Light", 400: "Regular", 500: "Medium", 600: "SemiBold", 700: "Bold", 800: "ExtraBold", 900: "Black" };
|
||||
return `${names[weight] ?? weight}${italic ? " Italic" : ""}`;
|
||||
};
|
||||
|
||||
/** Visible fonts grouped by face, as the sha256 list `prefs.fonts` carries. */
|
||||
export function selectionFrom(fonts: readonly UserFontRow[]): NonNullable<RenderPrefsV1["fonts"]> {
|
||||
const out: NonNullable<RenderPrefsV1["fonts"]> = {};
|
||||
for (const f of fonts) {
|
||||
if (f.hidden) continue;
|
||||
(out[f.face] ??= []).push(f.sha256);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The prefs to render and issue with: `prefs` plus the imported fonts for the faces this template draws. Fonts for
|
||||
* faces it does not use stay out, so an invoice only pins (and so only protects) the fonts it really uses. The
|
||||
* result is for rendering; keep the clean `prefs` for the page-setup controls and the saved default.
|
||||
*/
|
||||
export function withUserFonts(prefs: RenderPrefsV1, fonts: readonly UserFontRow[]): RenderPrefsV1 {
|
||||
const { fonts: _drop, ...rest } = prefs;
|
||||
const faces = catalogEntry(prefs.templateId).faces ?? [];
|
||||
const selection = selectionFrom(fonts);
|
||||
const used: NonNullable<RenderPrefsV1["fonts"]> = {};
|
||||
for (const face of faces) {
|
||||
const ids = selection[face];
|
||||
if (ids && ids.length > 0) used[face] = [...ids].sort();
|
||||
}
|
||||
return Object.keys(used).length > 0 ? { ...rest, fonts: used } : rest;
|
||||
}
|
||||
|
||||
// Settings and the invoice view are separate parts of the UI; this lets the list in one refresh the other.
|
||||
const listeners = new Set<() => void>();
|
||||
export const onUserFontsChanged = (cb: () => void): (() => void) => {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
};
|
||||
export const notifyUserFontsChanged = (): void => listeners.forEach((cb) => cb());
|
||||
Reference in New Issue
Block a user