From 379400d8755d4a39ee198ba12f28eb898e68b036 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 2 Mar 2026 03:36:35 +0100 Subject: [PATCH] Final weight and metrics adjustments --- build.py | 16 +------ scripts/metrics.py | 116 +++++++++++---------------------------------- scripts/scale.py | 2 +- 3 files changed, 30 insertions(+), 104 deletions(-) diff --git a/build.py b/build.py index 7e36286..cc8a547 100755 --- a/build.py +++ b/build.py @@ -36,16 +36,12 @@ ITALIC_VF = os.path.join(SRC_DIR, "Newsreader-Italic-VariableFont_opsz,wght.ttf VARIANTS = [ # (output_name, source_vf, wght, opsz) - ("Readerly-Regular", REGULAR_VF, 425, 9), + ("Readerly-Regular", REGULAR_VF, 430, 9), ("Readerly-Bold", REGULAR_VF, 550, 9), - ("Readerly-Italic", ITALIC_VF, 425, 9), + ("Readerly-Italic", ITALIC_VF, 430, 9), ("Readerly-BoldItalic", ITALIC_VF, 550, 9), ] -# Glyphs to clear — stacked diacritics that inflate head.yMax far beyond -# the design ascender. Aringacute (Ǻ/ǻ) is the sole outlier at 2268 units. -CLEAR_GLYPHS = ["Aringacute", "aringacute"] - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # HELPERS # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -174,20 +170,12 @@ def main(): scale_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "scale.py")) condense_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "condense.py")) - clear_code = "\n".join( - f'if {g!r} in f:\n' - f' f[{g!r}].clear()\n' - f' print(" Cleared: {g}")' - for g in CLEAR_GLYPHS - ) - for name in variant_names: ttf_path = os.path.join(MUTATED_DIR, f"{name}.ttf") sfd_path = os.path.join(MUTATED_DIR, f"{name}.sfd") print(f"Scaling: {name}") script = build_per_font_script(ttf_path, sfd_path, [ - ("Clearing problematic glyphs", clear_code), ("Scaling Y", scale_code), ("Condensing X", condense_code), ]) diff --git a/scripts/metrics.py b/scripts/metrics.py index d22c41e..e3d81e3 100644 --- a/scripts/metrics.py +++ b/scripts/metrics.py @@ -1,31 +1,17 @@ """ FontForge: Set Vertical Metrics ─────────────────────────────── -Follows the Google Fonts vertical-metrics methodology: - - • OS/2 Typo → controls line spacing (with USE_TYPO_METRICS / fsSelection bit 7) - • OS/2 Win → clipping boundary on Windows (must cover every glyph) - • hhea → line spacing + clipping on macOS/iOS +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 -import math f = fontforge.activeFont() -# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# CONFIGURATION -# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# Extra padding on Win and hhea metrics, as a fraction of UPM. -# Prevents clipping of glyphs that sit right at the bounding-box edge. -# 0.0 = trust the bounding boxes exactly -# 0.01 = 1% pad (conservative) -# 0.02 = 2% pad (safe for hinting artefacts / composites) -CLIP_MARGIN = 0.01 - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # HELPERS # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -34,7 +20,7 @@ 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): # skip empty glyphs (space, CR, …) + if bb != (0, 0, 0, 0): return bb return None @@ -42,8 +28,8 @@ def _bbox(name): def measure_chars(chars, *, axis="top"): """ Measure a set of reference characters. - axis="top" → return the highest yMax (ascenders, cap height) - axis="bottom" → return the lowest yMin (descenders) + 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 @@ -86,15 +72,11 @@ 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") -acc_h, acc_c = measure_chars("\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c8\u00c9\u00ca\u00cb", axis="top") -acd_h, acd_c = measure_chars("\u00c7\u015e\u0162", axis="bottom") for label, val, ch in [("Cap height", cap_h, cap_c), ("Ascender", asc_h, asc_c), - ("Accent top", acc_h, acc_c), ("x-height", xht_h, xht_c), - ("Descender", dsc_h, dsc_c), - ("Accent bot", acd_h, acd_c)]: + ("Descender", dsc_h, dsc_c)]: if val is not None: print(f" {label:12s} {int(val):>6} ('{ch}')") else: @@ -111,12 +93,11 @@ print(f" Highest glyph: {int(font_ymax):>6} ({ymax_name})") print(f" Lowest glyph: {int(font_ymin):>6} ({ymin_name})") # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# STEP 3 — Compute the three metric sets +# STEP 3 — Set OS/2 Typo to ink boundaries # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ upm = f.em -# Design top = tallest Latin ascender (fall back to cap height) design_top = asc_h if asc_h is not None else cap_h design_bot = dsc_h # negative value @@ -126,98 +107,55 @@ if design_top is None or design_bot is None: " Make sure your font contains basic Latin glyphs (H, b, p, etc.)." ) -# ── OS/2 Typo metrics ──────────────────────────────────────────────────────── -# These define line spacing when USE_TYPO_METRICS is on. -# Strategy: ascender and descender sit at the design's actual ink boundaries; -# lineGap absorbs all extra leading. This keeps the text vertically -# centred on the line, which matters for UI / web layout. - typo_ascender = int(round(design_top)) -typo_descender = int(round(design_bot)) # negative -typo_extent = typo_ascender - typo_descender # total ink span (positive) -typo_linegap = 0 +typo_descender = int(round(design_bot)) -# ── Win / hhea metrics ─────────────────────────────────────────────────────── -# Clipping boundaries. Based on the design ascender/descender with a small -# margin. Accented capitals and stacked diacritics may clip, but line -# height stays tight on all platforms. +f.os2_typoascent = typo_ascender +f.os2_typodescent = typo_descender +f.os2_typolinegap = 0 -margin = int(math.ceil(upm * CLIP_MARGIN)) - -win_ascent = int(math.ceil(design_top)) + margin -win_descent = int(math.ceil(abs(design_bot))) + margin - -hhea_ascent = win_ascent -hhea_descent = -win_descent # negative -hhea_linegap = 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 — Apply to font +# STEP 4 — USE_TYPO_METRICS (fsSelection bit 7) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Keep f.ascent / f.descent at their current values — they define UPM -# (f.ascent + f.descent = f.em) and must not be changed. - -# OS/2 table -f.os2_typoascent = typo_ascender -f.os2_typodescent = typo_descender -f.os2_typolinegap = typo_linegap -f.os2_winascent = win_ascent -f.os2_windescent = win_descent - -# hhea table -f.hhea_ascent = hhea_ascent -f.hhea_descent = hhea_descent -f.hhea_linegap = hhea_linegap - -# USE_TYPO_METRICS — fsSelection bit 7 -# FontForge exposes this differently across versions. We try three known paths. typo_metrics_set = False -# Method 1: dedicated boolean (FontForge ≥ 2020-ish) if hasattr(f, "os2_use_typo_metrics"): f.os2_use_typo_metrics = True typo_metrics_set = True -# Method 2: direct fsSelection manipulation (if exposed) if not typo_metrics_set and hasattr(f, "os2_fsselection"): f.os2_fsselection |= (1 << 7) typo_metrics_set = True -# Method 3: via the OS/2 version (some builds gate the flag on version ≥ 4) 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("⚠ Could not set USE_TYPO_METRICS programmatically.") - print(" → In Font Info → OS/2 → Misc, tick 'USE_TYPO_METRICS'.\n") + 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 + typo_linegap -hhea_line = hhea_ascent - hhea_descent + hhea_linegap -win_total = win_ascent + win_descent +typo_line = typo_ascender - typo_descender -print("\n─── Applied metrics ───\n") -print(f" UPM: {upm}\n") - -print(f" {'':18s} {'Ascender':>9s} {'Descender':>10s} {'LineGap':>8s} {'Total':>6s}") -print(f" {'─'*18} {'─'*9} {'─'*10} {'─'*8} {'─'*6}") -print(f" {'OS/2 Typo':18s} {typo_ascender:>9d} {typo_descender:>10d} {typo_linegap:>8d} {typo_line:>6d}") -print(f" {'hhea':18s} {hhea_ascent:>9d} {hhea_descent:>10d} {hhea_linegap:>8d} {hhea_line:>6d}") -print(f" {'OS/2 Win':18s} {win_ascent:>9d} {'-'+str(win_descent):>10s} {'n/a':>8s} {win_total:>6d}") - -print(f"\n Effective line height: {typo_line} ({typo_line/upm:.2f}× UPM)") -print(f" Design ink span: {typo_extent} ({typo_extent/upm:.2f}× UPM)") -print(f" Clipping headroom: +{win_ascent - int(round(font_ymax))} above, " - f"+{win_descent - int(round(abs(font_ymin)))} below") +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"\n Cap height: {int(cap_h)}") + print(f" Cap height: {int(cap_h)}") if xht_h is not None: print(f" x-height: {int(xht_h)}") -print("\nDone. Review in Font Info → OS/2 and Font Info → General.") +print("\nDone.") diff --git a/scripts/scale.py b/scripts/scale.py index c4e7cf1..4a87f36 100644 --- a/scripts/scale.py +++ b/scripts/scale.py @@ -25,7 +25,7 @@ f = fontforge.activeFont() # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SCALE_X = 1.03 -SCALE_Y = 1.10 +SCALE_Y = 1.08 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # APPLY