Fix x-height overshoot for hinting
This commit is contained in:
@@ -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`.
|
||||||
|
|||||||
72
build.py
72
build.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user