162 lines
7.1 KiB
Python
162 lines
7.1 KiB
Python
"""
|
|
FontForge: Set Vertical Metrics
|
|
───────────────────────────────
|
|
Measures design landmarks, sets OS/2 Typo metrics to the ink boundaries,
|
|
and enables USE_TYPO_METRICS. Win/hhea are set to initial values here
|
|
but will be overridden by lineheight.py.
|
|
|
|
Run inside FontForge (File → Execute Script → paste, or fontforge -script).
|
|
"""
|
|
|
|
import fontforge
|
|
|
|
f = fontforge.activeFont()
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# HELPERS
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
def _bbox(name):
|
|
"""Return bounding box (xmin, ymin, xmax, ymax) or None."""
|
|
if name in f and f[name].isWorthOutputting():
|
|
bb = f[name].boundingBox()
|
|
if bb != (0, 0, 0, 0):
|
|
return bb
|
|
return None
|
|
|
|
|
|
def measure_chars(chars, *, axis="top"):
|
|
"""
|
|
Measure a set of reference characters.
|
|
axis="top" → return the highest yMax
|
|
axis="bottom" → return the lowest yMin
|
|
Returns (value, display_char) or (None, None).
|
|
"""
|
|
idx = 3 if axis == "top" else 1
|
|
pick = max if axis == "top" else min
|
|
hits = []
|
|
for ch in chars:
|
|
name = fontforge.nameFromUnicode(ord(ch))
|
|
bb = _bbox(name)
|
|
if bb is not None:
|
|
hits.append((bb[idx], ch))
|
|
if not hits:
|
|
return None, None
|
|
return pick(hits, key=lambda t: t[0])
|
|
|
|
|
|
def scan_font_extremes():
|
|
"""Walk every output glyph; return (yMax, yMin, max_name, min_name)."""
|
|
y_max, y_min = 0, 0
|
|
max_nm, min_nm = None, None
|
|
for g in f.glyphs():
|
|
if not g.isWorthOutputting():
|
|
continue
|
|
bb = g.boundingBox()
|
|
if bb == (0, 0, 0, 0):
|
|
continue
|
|
if bb[3] > y_max:
|
|
y_max, max_nm = bb[3], g.glyphname
|
|
if bb[1] < y_min:
|
|
y_min, min_nm = bb[1], g.glyphname
|
|
return y_max, y_min, max_nm, min_nm
|
|
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# STEP 1 — Measure design landmarks
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
print("─── Design landmarks ───\n")
|
|
|
|
cap_h, cap_c = measure_chars("HIOXE", axis="top")
|
|
asc_h, asc_c = measure_chars("bdfhkl", axis="top")
|
|
xht_h, xht_c = measure_chars("xzouv", axis="top")
|
|
dsc_h, dsc_c = measure_chars("gpqyj", axis="bottom")
|
|
|
|
for label, val, ch in [("Cap height", cap_h, cap_c),
|
|
("Ascender", asc_h, asc_c),
|
|
("x-height", xht_h, xht_c),
|
|
("Descender", dsc_h, dsc_c)]:
|
|
if val is not None:
|
|
print(f" {label:12s} {int(val):>6} ('{ch}')")
|
|
else:
|
|
print(f" {label:12s} {'N/A':>6}")
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# STEP 2 — Full-font bounding-box scan
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
print("\n─── Full font scan ───\n")
|
|
|
|
font_ymax, font_ymin, ymax_name, ymin_name = scan_font_extremes()
|
|
print(f" Highest glyph: {int(font_ymax):>6} ({ymax_name})")
|
|
print(f" Lowest glyph: {int(font_ymin):>6} ({ymin_name})")
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# STEP 3 — Set OS/2 Typo to ink boundaries
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
upm = f.em
|
|
|
|
design_top = asc_h if asc_h is not None else cap_h
|
|
design_bot = dsc_h # negative value
|
|
|
|
if design_top is None or design_bot is None:
|
|
raise SystemExit(
|
|
"ERROR: Could not measure ascender/cap-height or descender.\n"
|
|
" Make sure your font contains basic Latin glyphs (H, b, p, etc.)."
|
|
)
|
|
|
|
typo_ascender = int(round(design_top))
|
|
typo_descender = int(round(design_bot))
|
|
|
|
f.os2_typoascent = typo_ascender
|
|
f.os2_typodescent = typo_descender
|
|
f.os2_typolinegap = 0
|
|
|
|
# Win/hhea set to same initial values; lineheight.py overrides these.
|
|
f.os2_winascent = typo_ascender
|
|
f.os2_windescent = abs(typo_descender)
|
|
f.hhea_ascent = typo_ascender
|
|
f.hhea_descent = typo_descender
|
|
f.hhea_linegap = 0
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# STEP 4 — USE_TYPO_METRICS (fsSelection bit 7)
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
typo_metrics_set = False
|
|
|
|
if hasattr(f, "os2_use_typo_metrics"):
|
|
f.os2_use_typo_metrics = True
|
|
typo_metrics_set = True
|
|
|
|
if not typo_metrics_set and hasattr(f, "os2_fsselection"):
|
|
f.os2_fsselection |= (1 << 7)
|
|
typo_metrics_set = True
|
|
|
|
if not typo_metrics_set:
|
|
if hasattr(f, "os2_version") and f.os2_version < 4:
|
|
f.os2_version = 4
|
|
|
|
if not typo_metrics_set:
|
|
print(" WARNING: Could not set USE_TYPO_METRICS programmatically.")
|
|
print(" -> In Font Info -> OS/2 -> Misc, tick 'USE_TYPO_METRICS'.\n")
|
|
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
# STEP 5 — Report
|
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
typo_line = typo_ascender - typo_descender
|
|
|
|
print(f"\n─── Applied metrics ───\n")
|
|
print(f" UPM: {upm}")
|
|
print(f" Typo: {typo_ascender} / {typo_descender} (ink span: {typo_line}, {typo_line/upm:.2f}x UPM)")
|
|
|
|
if cap_h is not None:
|
|
print(f" Cap height: {int(cap_h)}")
|
|
if xht_h is not None:
|
|
print(f" x-height: {int(xht_h)}")
|
|
|
|
print("\nDone.")
|