"""Regenerate font coverage after replacing the bundled NanumSquare WOFF2 files. Requires fonttools[woff]. Normal app builds use the checked-in CSS, not Python. Run: python3 scripts/generate-nanumsquare-css.py [--check] """ import argparse import hashlib import unicodedata from pathlib import Path from fontTools.pens.boundsPen import BoundsPen from fontTools.ttLib import TTFont ROOT = Path(__file__).resolve().parents[1] FONTS = ROOT / "src/renderer/public/assets/fonts" OUTPUT = ROOT / "src/renderer/src/styles/nanumSquare.css" FACES = [(300, "NanumSquareL"), (400, "NanumSquare"), (700, "NanumSquareB"), (800, "NanumSquareEB")] # Hangul fillers are intentionally invisible, like whitespace and controls. FILLERS = {0x115F, 0x1160, 0x3164, 0xFFA0} def coverage(font): glyphs = font.getGlyphSet() supported = [] for code, name in sorted(font.getBestCmap().items()): pen = BoundsPen(glyphs) glyphs[name].draw(pen) intentional = chr(code).isspace() or unicodedata.category(chr(code)) in {"Cc", "Cf"} or code in FILLERS if pen.bounds is not None or intentional: supported.append(code) return supported def unicode_ranges(codes): ranges = [] start = end = codes[0] for code in codes[1:]: if code == end + 1: end = code else: ranges.append((start, end)) start = end = code ranges.append((start, end)) return [f"U+{start:X}" if start == end else f"U+{start:X}-{end:X}" for start, end in ranges] def generate(): sections = [ "/* Generated by scripts/generate-nanumsquare-css.py. Do not edit manually.\n" " * Empty glyphs are excluded so the existing system-font fallback can render them.\n" " * Font binaries and visible glyphs are unchanged.\n */" ] for weight, name in FACES: file = FONTS / f"{name}.woff2" with TTFont(file) as font: ranges = unicode_ranges(coverage(font)) lines = [", ".join(ranges[index:index + 10]) for index in range(0, len(ranges), 10)] sections.append( f"/* {file.name} sha256: {hashlib.sha256(file.read_bytes()).hexdigest()} */\n" "@font-face {\n" " font-family: 'NanumSquare';\n" f" src: url('/assets/fonts/{file.name}') format('woff2');\n" f" font-weight: {weight};\n" " font-style: normal;\n" " font-display: swap;\n" " unicode-range:\n " + ",\n ".join(lines) + ";\n}" ) return "\n\n".join(sections) + "\n" if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true") args = parser.parse_args() generated = generate() if args.check: if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != generated: raise SystemExit("NanumSquare coverage CSS is stale; regenerate it") print("NanumSquare coverage matches all four font files") else: OUTPUT.parent.mkdir(parents=True, exist_ok=True) OUTPUT.write_text(generated, encoding="utf-8") print(f"Generated {OUTPUT.relative_to(ROOT)}")