import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const fontsDir = join(__dirname, "..", "public", "fonts"); const outputWoff2 = join(fontsDir, "lxgwwenkaimono-medium.woff2"); const sourceTtf = join(__dirname, "..", "assets", "fonts", "LXGWWenKaiMono-Medium.ttf"); /** Full CJK font woff2 is typically 7–10 MB; reject obvious subset/corrupt output. */ const MIN_WOFF2_BYTES = 5_000_000; if (!existsSync(sourceTtf)) { console.warn(`[prepare-font] source not found: ${sourceTtf}`); process.exit(0); } mkdirSync(fontsDir, { recursive: true }); function removePublicTtfCopies() { for (const name of readdirSync(fontsDir)) { if (name.toLowerCase().endsWith(".ttf")) { const path = join(fontsDir, name); unlinkSync(path); console.log(`[prepare-font] removed public TTF copy (woff2 only): ${path}`); } } } function woff2LooksValid() { if (!existsSync(outputWoff2)) return false; return readFileSync(outputWoff2).length >= MIN_WOFF2_BYTES; } function verifyGlyphCount() { const py = [ "from fontTools.ttLib import TTFont", "import sys", "src, out = sys.argv[1], sys.argv[2]", "source_glyphs = len(TTFont(src).getGlyphOrder())", "output_glyphs = len(TTFont(out).getGlyphOrder())", "if source_glyphs != output_glyphs:", "\traise SystemExit(f'glyph count mismatch: source={source_glyphs}, output={output_glyphs}')", "print(output_glyphs)", ].join("\n"); const count = execFileSync("python", ["-c", py, sourceTtf, outputWoff2], { encoding: "utf8", }).trim(); console.log(`[prepare-font] verified full glyph set (${count} glyphs)`); } removePublicTtfCopies(); if (woff2LooksValid()) { const sourceMtime = statSync(sourceTtf).mtimeMs; const outputMtime = statSync(outputWoff2).mtimeMs; if (outputMtime >= sourceMtime) { const sizeMb = (readFileSync(outputWoff2).length / (1024 * 1024)).toFixed(2); console.log(`[prepare-font] up to date ${outputWoff2} (${sizeMb} MB, woff2 only)`); process.exit(0); } } if (existsSync(outputWoff2) && !woff2LooksValid()) { console.warn("[prepare-font] removing invalid woff2 (< 5 MB, likely subset or corrupt)"); unlinkSync(outputWoff2); } console.log( "[prepare-font] converting full font to woff2 (no glyph subsetting, glyf/loca/hmtx transforms)...", ); const py = [ "from fontTools.ttLib.woff2 import compress", "import sys", "compress(sys.argv[1], sys.argv[2], transform_tables={'glyf', 'loca', 'hmtx'})", ].join("\n"); execFileSync("python", ["-c", py, sourceTtf, outputWoff2], { stdio: "inherit" }); const sizeBytes = readFileSync(outputWoff2).length; if (sizeBytes < MIN_WOFF2_BYTES) { throw new Error( `[prepare-font] woff2 output too small (${sizeBytes} bytes); conversion likely failed`, ); } verifyGlyphCount(); removePublicTtfCopies(); const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2); console.log(`[prepare-font] wrote ${outputWoff2} (${sizeMb} MB, full glyph set, woff2 only)`);