Static TTF fonts (IBM Plex Sans/Mono, Noto Devanagari/Kannada) with a fontkit-generated manifest, a verifier and OFL licences. Every family registers a normal and italic source so fallback stacks cannot throw. Adds A4/Letter geometry with margin presets, an exact column allocator, deterministic Indian-grouping money and date formatting, text sanitising and a pure buildRenderModel.
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
// Shared helpers for the font manifest builder and verifier (Node only).
|
|
import { createHash } from "node:crypto";
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as fontkit from "fontkit";
|
|
|
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
export const FONTS_DIR = path.join(ROOT, "public/fonts");
|
|
export const MANIFEST_PATH = path.join(FONTS_DIR, "manifest.json");
|
|
|
|
export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
|
|
/** Folders under public/fonts that hold bundled families (everything that is a directory). */
|
|
export function familyFolders() {
|
|
return readdirSync(FONTS_DIR)
|
|
.filter((name) => statSync(path.join(FONTS_DIR, name)).isDirectory())
|
|
.sort();
|
|
}
|
|
|
|
export function ttfFiles(folder) {
|
|
return readdirSync(path.join(FONTS_DIR, folder))
|
|
.filter((name) => /\.ttf$/i.test(name))
|
|
.sort();
|
|
}
|
|
|
|
/** First four bytes as a tag: "\0\1\0\0" is reported as "0x00010000". */
|
|
export function sfntTag(bytes) {
|
|
const b = bytes.subarray(0, 4);
|
|
if (b[0] === 0 && b[1] === 1 && b[2] === 0 && b[3] === 0) return "0x00010000";
|
|
return String.fromCharCode(...b);
|
|
}
|
|
|
|
/** Raw OS/2 fsType (fontkit only exposes decoded flags). */
|
|
function rawFsType(bytes) {
|
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const numTables = dv.getUint16(4);
|
|
for (let i = 0; i < numTables; i++) {
|
|
const rec = 12 + i * 16;
|
|
const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
|
|
if (tag === "OS/2") return dv.getUint16(dv.getUint32(rec + 8) + 8);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Reads a TTF with fontkit and returns the facts the manifest records. */
|
|
export function inspectFont(folder, file) {
|
|
const bytes = new Uint8Array(readFileSync(path.join(FONTS_DIR, folder, file)));
|
|
const font = fontkit.create(Buffer.from(bytes));
|
|
if (font.fonts) throw new Error(`${folder}/${file}: font collections are not supported`);
|
|
const os2 = font["OS/2"];
|
|
return {
|
|
bytes,
|
|
tag: sfntTag(bytes),
|
|
variable: Boolean(font.directory?.tables?.fvar),
|
|
// Name ID 16 (typographic family) groups SemiBold etc. under the base family.
|
|
familyName: font.name?.records?.preferredFamily?.en ?? font.familyName,
|
|
info: {
|
|
file,
|
|
weight: os2.usWeightClass,
|
|
style: os2.fsSelection.italic || os2.fsSelection.oblique ? "italic" : "normal",
|
|
sha256: sha256(bytes),
|
|
fsType: rawFsType(bytes),
|
|
upm: font.unitsPerEm,
|
|
ascent: font.ascent,
|
|
descent: font.descent,
|
|
lineGap: font.lineGap,
|
|
capHeight: font.capHeight,
|
|
xHeight: font.xHeight,
|
|
hasRupee: font.hasGlyphForCodePoint(0x20b9),
|
|
hasTabularNums: font.availableFeatures.includes("tnum"),
|
|
},
|
|
};
|
|
}
|
|
|
|
export const WEIGHT_WORDS = {
|
|
Thin: 100, ExtraLight: 200, Light: 300, Regular: 400, Italic: 400, Text: 450,
|
|
Medium: 500, SemiBold: 600, Bold: 700, ExtraBold: 800, Black: 900,
|
|
};
|