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.
77 lines
3.3 KiB
JavaScript
77 lines
3.3 KiB
JavaScript
// Checks public/fonts against manifest.json. Exits non-zero with readable messages on any problem.
|
|
// Run: npm run fonts:verify
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { FONTS_DIR, MANIFEST_PATH, WEIGHT_WORDS, familyFolders, inspectFont, ttfFiles } from "./lib.mjs";
|
|
|
|
const problems = [];
|
|
const fail = (where, msg) => problems.push(`${where}: ${msg}`);
|
|
|
|
if (!existsSync(MANIFEST_PATH)) {
|
|
console.error("public/fonts/manifest.json is missing; run node scripts/fonts/build-manifest.mjs");
|
|
process.exit(1);
|
|
}
|
|
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
if (manifest.version !== 1) fail("manifest", `unsupported version ${manifest.version}`);
|
|
|
|
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
const listed = new Set();
|
|
let count = 0;
|
|
|
|
for (const fam of manifest.families) {
|
|
const dir = `public/fonts/${fam.folder}`;
|
|
if (slug(fam.family) !== fam.folder) fail(dir, `folder does not match family "${fam.family}"`);
|
|
if (!existsSync(path.join(FONTS_DIR, fam.folder, fam.licenseFile))) {
|
|
fail(dir, `licence file ${fam.licenseFile} is missing`);
|
|
}
|
|
for (const entry of fam.fonts) {
|
|
const where = `${fam.folder}/${entry.file}`;
|
|
listed.add(where);
|
|
count++;
|
|
if (!existsSync(path.join(FONTS_DIR, fam.folder, entry.file))) {
|
|
fail(where, "listed in the manifest but not on disk");
|
|
continue;
|
|
}
|
|
let f;
|
|
try {
|
|
f = inspectFont(fam.folder, entry.file);
|
|
} catch (err) {
|
|
fail(where, `cannot be parsed: ${err.message}`);
|
|
continue;
|
|
}
|
|
if (f.tag !== "0x00010000" && f.tag !== "true") {
|
|
fail(where, `not a TrueType font (magic "${f.tag}"); WOFF, WOFF2 and OpenType/CFF must not be bundled for PDFs`);
|
|
}
|
|
if (f.variable) fail(where, "variable font (has fvar); react-pdf would embed only the default instance");
|
|
if (f.info.sha256 !== entry.sha256) fail(where, "sha256 differs from the manifest; rebuild it if the change is intended");
|
|
if (f.info.fsType !== 0 && f.info.fsType !== 8) {
|
|
fail(where, `fsType ${f.info.fsType} does not allow embedding (need 0 or 8)`);
|
|
}
|
|
if (f.familyName !== fam.family) fail(where, `family is "${f.familyName}", manifest says "${fam.family}"`);
|
|
if (f.info.weight !== entry.weight || f.info.style !== entry.style) {
|
|
fail(where, `weight/style ${f.info.weight}/${f.info.style} differs from the manifest ${entry.weight}/${entry.style}`);
|
|
}
|
|
const word = entry.file.replace(/\.ttf$/i, "").split("-").pop();
|
|
const expected = WEIGHT_WORDS[word];
|
|
if (expected !== undefined && word !== "Text" && expected !== f.info.weight) {
|
|
fail(where, `file name says ${word} (${expected}) but OS/2 weight is ${f.info.weight}`);
|
|
}
|
|
if (/Italic$/.test(word) !== (f.info.style === "italic")) {
|
|
fail(where, `file name and OS/2 disagree about italic (${f.info.style})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const folder of familyFolders()) {
|
|
for (const file of ttfFiles(folder)) {
|
|
if (!listed.has(`${folder}/${file}`)) fail(`${folder}/${file}`, "TTF on disk but not in the manifest");
|
|
}
|
|
}
|
|
|
|
if (problems.length) {
|
|
console.error(`fonts:verify found ${problems.length} problem(s):`);
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`fonts:verify OK: ${manifest.families.length} families, ${count} fonts`);
|