#!/usr/bin/env python3 # hangulmath-metrics.py -- part of the hangulmath package # Copyright (C) 2026 Inkyu Park # Distributed under the LaTeX Project Public License 1.3c or later. """Generate a hangulmath metrics file for a given font. Requires Python 3.10+ and fontTools (pip install fonttools). Usage: python3 hangulmath-metrics.py "Nanum Gothic" -o nanum.def # font name (fc-match) python3 hangulmath-metrics.py /path/to/font.otf -o myfont.def The output (default: hangulmath-metrics.def) records, for each of the 24 jamo, the left/right ink bounds in em, plus a common baseline shift and a recommended scale. Load it with \\usepackage[font={...}, metrics=myfont]{hangulmath} """ import argparse import statistics import subprocess import sys from pathlib import Path from fontTools.pens.boundsPen import BoundsPen from fontTools.ttLib import TTCollection, TTFont CONSONANTS = [0x3131, 0x3134, 0x3137, 0x3139, 0x3141, 0x3142, 0x3145, 0x3147, 0x3148, 0x314A, 0x314B, 0x314C, 0x314D, 0x314E] VOWELS = [0x314F, 0x3151, 0x3153, 0x3155, 0x3157, 0x315B, 0x315C, 0x3160, 0x3161, 0x3163] DOUBLE_CONSONANTS = [0x3132, 0x3138, 0x3143, 0x3146, 0x3149] COMPOUND_VOWELS = [0x3150, 0x3152, 0x3154, 0x3156, 0x3158, 0x3159, 0x315A, 0x315D, 0x315E, 0x315F, 0x3162] # Target height of a consonant, in em of the surrounding text font # (Latin Modern: x-height 0.43em, cap height 0.68em). TARGET_CONSONANT_HEIGHT = 0.60 def resolve(font: str) -> tuple[str, str | None]: """Return (file path, family name to pick inside a .ttc).""" if Path(font).exists(): return font, None path = subprocess.run(["fc-match", "-f", "%{file}", font], capture_output=True, text=True, check=True).stdout return path, font def load(path: str, family: str | None) -> TTFont: if path.lower().endswith((".ttc", ".otc")): fonts = TTCollection(path).fonts for f in fonts: if family and f["name"].getDebugName(1) == family: return f return fonts[0] return TTFont(path) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("font", help="font name or file path") ap.add_argument("-o", "--output", default="hangulmath-metrics.def") args = ap.parse_args() path, family = resolve(args.font) font = load(path, family) upm = font["head"].unitsPerEm cmap = font.getBestCmap() glyphs = font.getGlyphSet() bounds = {} for code in CONSONANTS + VOWELS + DOUBLE_CONSONANTS + COMPOUND_VOWELS: if code not in cmap: sys.exit(f"U+{code:04X} not in font {args.font}") pen = BoundsPen(glyphs) glyphs[cmap[code]].draw(pen) x0, y0, x1, y1 = (v / upm for v in pen.bounds) bounds[code] = (x0, y0, x1, y1, glyphs[cmap[code]].width / upm) shift = statistics.median(bounds[c][1] for c in CONSONANTS) height = statistics.median(bounds[c][3] - bounds[c][1] for c in CONSONANTS) scale = TARGET_CONSONANT_HEIGHT / height lines = [ f"%% hangulmath metrics for: {args.font}", f"%% generated by hangulmath-metrics.py from {path}", f"\\def\\hm@baseshift{{{shift:.4f}}}", f"\\def\\hm@defaultscale{{{scale:.3f}}}", ] for code, (x0, _, x1, _, adv) in bounds.items(): lines.append(f"\\hm@metric{{{code:04X}}}{{{x0:.4f}}}{{{adv - x1:.4f}}}" f"% {chr(code)}") Path(args.output).write_text("\n".join(lines) + "\n\\endinput\n", encoding="utf-8") print(f"wrote {args.output} (shift={shift:.3f}em, scale={scale:.3f})") if __name__ == "__main__": main()