// 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`);