1
0

Fix x-height overshoot for hinting

This commit is contained in:
2026-03-14 12:38:28 +01:00
parent 139b14dbf6
commit f5aac0701f
3 changed files with 66 additions and 11 deletions

View File

@@ -22,7 +22,6 @@ To get to the final result, I decided to use the variable font and work on it. T
After running `build.py`, you should get: After running `build.py`, you should get:
- `out/sfd`: FontForge source files (generated)
- `out/ttf`: final TTF fonts (generated) - `out/ttf`: final TTF fonts (generated)
## Prerequisites ## Prerequisites
@@ -87,4 +86,4 @@ To customize the font family name, disable old-style kerning, or skip outline fi
python3 build.py --customize python3 build.py --customize
``` ```
The build script (`build.py`) uses `fontTools` and FontForge to transform the Newsreader variable fonts into Readerly. Configuration and step-by-step details live in the header comments of `build.py`. The build script (`build.py`) uses `fontTools` and FontForge to transform the Newsreader variable fonts into Readerly. After export, it post-processes the TTFs: clamping x-height overshoots that cause uneven rendering on e-ink, normalizing style flags, and autohinting with `ttfautohint` for Kobo's FreeType renderer. Configuration and step-by-step details live in the header comments of `build.py`.

View File

@@ -1 +1 @@
1.3 1.3

View File

@@ -7,7 +7,8 @@ Orchestrates the full font build pipeline:
1. Instances variable fonts into static TTFs (fontTools.instancer) 1. Instances variable fonts into static TTFs (fontTools.instancer)
2. Applies vertical scale (scale.py) via FontForge 2. Applies vertical scale (scale.py) via FontForge
3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py) 3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py)
4. Exports to SFD and TTF → ./out/sfd/ and ./out/ttf/ 4. Exports to TTF → ./out/ttf/
5. Post-processes TTFs: x-height overshoot clamping, style flags, autohinting
Uses FontForge (detected automatically). Uses FontForge (detected automatically).
Run with: python3 build.py Run with: python3 build.py
@@ -40,7 +41,6 @@ import textwrap
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
SRC_DIR = os.path.join(ROOT_DIR, "src") SRC_DIR = os.path.join(ROOT_DIR, "src")
OUT_DIR = os.path.join(ROOT_DIR, "out") OUT_DIR = os.path.join(ROOT_DIR, "out")
OUT_SFD_DIR = os.path.join(OUT_DIR, "sfd") # generated FontForge sources
OUT_TTF_DIR = os.path.join(OUT_DIR, "ttf") # generated TTFs OUT_TTF_DIR = os.path.join(OUT_DIR, "ttf") # generated TTFs
REGULAR_VF = os.path.join(SRC_DIR, "Newsreader-VariableFont_opsz,wght.ttf") REGULAR_VF = os.path.join(SRC_DIR, "Newsreader-VariableFont_opsz,wght.ttf")
@@ -101,6 +101,11 @@ AUTOHINT_OPTS = [
"--increase-x-height=0", "--increase-x-height=0",
] ]
# Glyphs whose x-height overshoot is an outlier (+12 vs the standard +22).
# The inconsistent overshoot lands between the hinter's snap zones, causing
# these glyphs to render taller than their neighbors on low-res e-ink.
CLAMP_XHEIGHT_GLYPHS = ["u", "uogonek"]
# Step 3: Naming and style metadata (used by the rename step) # Step 3: Naming and style metadata (used by the rename step)
STYLE_MAP = { STYLE_MAP = {
"Regular": ("Regular", "Book", 400), "Regular": ("Regular", "Book", 400),
@@ -601,6 +606,62 @@ def clean_ttf_degenerate_contours(ttf_path):
font.close() font.close()
def clamp_xheight_overshoot(ttf_path):
"""Clamp outlier x-height overshoots in a TTF in-place.
Some glyphs (e.g. 'u') have a smaller overshoot than the standard
round overshoot, landing between the hinter's snap zones. This
flattens them to the true x-height measured from flat-topped glyphs.
"""
try:
from fontTools.ttLib import TTFont
except Exception:
print(" [warn] Skipping x-height clamp: fontTools not available", file=sys.stderr)
return
font = TTFont(ttf_path)
glyf = font["glyf"]
# Measure x-height from flat-topped reference glyphs.
xheight = 0
for ref in ("x", "v"):
if ref not in glyf:
continue
coords = glyf[ref].coordinates
if coords:
ymax = max(c[1] for c in coords)
if ymax > xheight:
xheight = ymax
if xheight == 0:
font.close()
return
clamped = []
for name in CLAMP_XHEIGHT_GLYPHS:
if name not in glyf:
continue
glyph = glyf[name]
coords = glyph.coordinates
if not coords:
continue
ymax = max(c[1] for c in coords)
if ymax <= xheight:
continue
glyph.coordinates = type(coords)(
[(x, min(y, xheight)) for x, y in coords]
)
glyph_set = font.getGlyphSet()
if hasattr(glyph, "recalcBounds"):
glyph.recalcBounds(glyph_set)
clamped.append(name)
if clamped:
font.save(ttf_path)
print(f" Clamped x-height overshoot for: {', '.join(clamped)} (xh={xheight})")
font.close()
def fix_ttf_style_flags(ttf_path, style_suffix): def fix_ttf_style_flags(ttf_path, style_suffix):
"""Normalize OS/2 fsSelection and head.macStyle for style linking.""" """Normalize OS/2 fsSelection and head.macStyle for style linking."""
try: try:
@@ -814,7 +875,6 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True, outline_fix=True):
# Step 4: Export to out/sfd and out/ttf # Step 4: Export to out/sfd and out/ttf
print("\n── Step 4: Export ──\n") print("\n── Step 4: Export ──\n")
os.makedirs(OUT_SFD_DIR, exist_ok=True)
os.makedirs(OUT_TTF_DIR, exist_ok=True) os.makedirs(OUT_TTF_DIR, exist_ok=True)
for name in variant_names: for name in variant_names:
@@ -822,22 +882,18 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True, outline_fix=True):
ttf_path = os.path.join(OUT_TTF_DIR, f"{name}.ttf") ttf_path = os.path.join(OUT_TTF_DIR, f"{name}.ttf")
style_suffix = name.split("-")[-1] if "-" in name else "Regular" style_suffix = name.split("-")[-1] if "-" in name else "Regular"
# Copy final SFD to out/sfd/
shutil.copy2(sfd_path, os.path.join(OUT_SFD_DIR, f"{name}.sfd"))
print(f" -> {OUT_SFD_DIR}/{name}.sfd")
# Export TTF # Export TTF
script = build_export_script(sfd_path, ttf_path, old_kern=old_kern) script = build_export_script(sfd_path, ttf_path, old_kern=old_kern)
run_fontforge_script(script) run_fontforge_script(script)
if outline_fix: if outline_fix:
clean_ttf_degenerate_contours(ttf_path) clean_ttf_degenerate_contours(ttf_path)
clamp_xheight_overshoot(ttf_path)
fix_ttf_style_flags(ttf_path, style_suffix) fix_ttf_style_flags(ttf_path, style_suffix)
autohint_ttf(ttf_path) autohint_ttf(ttf_path)
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(" Build complete!") print(" Build complete!")
print(f" SFD fonts are in: {OUT_SFD_DIR}/")
print(f" TTF fonts are in: {OUT_TTF_DIR}/") print(f" TTF fonts are in: {OUT_TTF_DIR}/")
print("=" * 60) print("=" * 60)