1
0

Update hinting again

This commit is contained in:
2026-03-28 23:37:04 +01:00
parent 83a84b7941
commit e3b8fea2a5
2 changed files with 82 additions and 48 deletions

130
build.py
View File

@@ -90,20 +90,32 @@ SELECTION_HEIGHT = 1.3
ASCENDER_RATIO = 0.8 ASCENDER_RATIO = 0.8
# Step 4: ttfautohint options (hinting for Kobo's FreeType renderer) # Step 4: ttfautohint options (hinting for Kobo's FreeType renderer)
# - Kobo uses FreeType grayscale on e-ink, where gray pixels are very # - Kobo uses FreeType grayscale, so the 1st char of --stem-width-mode
# visible. Strong mode (s) snaps stems to integer pixels, minimising # matters: n=natural (least distortion), q=quantized, s=strong.
# gray anti-aliasing at the cost of some shape distortion. # - x-height snapping is disabled to avoid inconsistent glyph heights.
# - Other options are left at ttfautohint defaults.
AUTOHINT_OPTS = [ AUTOHINT_OPTS = [
"--no-info", "--no-info",
"--stem-width-mode=qss", "--stem-width-mode=nss",
"--increase-x-height=0",
'--x-height-snapping-exceptions=-',
] ]
# Serif shelf detection: points within this distance (in font units) # Baseline alignment: deepen the bottom anti-aliasing of non-serifed
# inward from a blue zone edge get `left`/`right` direction hints so # glyphs via hinting-only touch deltas (no outline changes). This
# ttfautohint creates explicit segments instead of interpolating them. # shifts their bottom points down during rasterization so they produce
# This fixes gray ghosting at serif feet (bottom) and tops (e.g. w, v). # more gray below the baseline, visually matching serifed characters.
SERIF_SHELF_INSET = 160 # how far the shelf can be from the blue zone # Each entry is (shift_px, ppem_min, ppem_max). Shifts are in pixels
# (multiples of 1/8, max 1.0). Set to empty list to disable.
BASELINE_HINT_SHIFTS = [
(0.125, 6, 53),
]
# Per-glyph Y ceiling: cap the top of specific glyphs to reduce
# oversized or awkward serifs. (glyph_name, max_y)
# Points above max_y are clamped down to max_y.
GLYPH_Y_CEILING = [
("u", 1062), # flatten tiny top serif tips to platform level
]
# Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units). # Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units).
# Negative values tighten spacing. These are added on top of any existing # Negative values tighten spacing. These are added on top of any existing
@@ -754,54 +766,75 @@ def add_kern_pairs(ttf_path):
print(f" Added {count} kern pair(s) to GPOS") print(f" Added {count} kern pair(s) to GPOS")
def _generate_serif_ctrl(ttf_path):
"""Generate ttfautohint control instructions for serif shelf points.
Scans the font for glyph points near (but not on) blue zone edges def apply_glyph_y_ceiling(ttf_path):
— the serif "shelves" that ttfautohint doesn't detect as segments. """Clamp glyph points above a Y ceiling down to the ceiling value."""
Without explicit hints these get interpolated to fractional pixel if not GLYPH_Y_CEILING:
positions, causing gray ghosting on e-ink. return
Bottom shelves (near baseline y=0) get `left` direction hints.
Top shelves (near x-height) get `right` direction hints.
"""
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
font = TTFont(ttf_path) font = TTFont(ttf_path)
glyf = font["glyf"] glyf = font["glyf"]
os2 = font["OS/2"] modified = []
x_height = getattr(os2, "sxHeight", None) or 0
# Blue zone edges: (zone_y, tolerance, inset, direction) for glyph_name, max_y in GLYPH_Y_CEILING:
# - tolerance: how close a glyph's "flat edge" must be to zone_y g = glyf.get(glyph_name)
# - inset: how far inward the shelf can be from the flat edge if not g or not g.numberOfContours or g.numberOfContours <= 0:
# - direction: left = bottom shelf, right = top shelf continue
zones = [ coords = g.coordinates
# (zone_y, tolerance, inset, direction) clamped = 0
# tolerance: how close a glyph's flat edge must be to count for j in range(len(coords)):
# inset: how far the shelf can be from the zone if coords[j][1] > max_y:
# Shelf points are between zone_y±inset and zone_y±tolerance. coords[j] = (coords[j][0], max_y)
(0, 0, SERIF_SHELF_INSET, "left"), clamped += 1
] if clamped:
if x_height: modified.append(f"{glyph_name}({clamped}pts)")
zones.append((x_height, 25, SERIF_SHELF_INSET, "right"))
if modified:
font.save(ttf_path)
print(f" Clamped Y ceiling: {', '.join(modified)}")
font.close()
def _generate_baseline_shift_ctrl(ttf_path):
"""Generate touch deltas to deepen bottom anti-aliasing of non-serifed glyphs.
For lowercase glyphs without a flat baseline (no serif foot), shifts
the bottom-most points down during rasterization. Uses graduated
shifts from BASELINE_HINT_SHIFTS — stronger at small ppem sizes
where alignment is most noticeable. No outline changes.
"""
if not BASELINE_HINT_SHIFTS:
return ""
from fontTools.ttLib import TTFont
font = TTFont(ttf_path)
glyf = font["glyf"]
cmap = font.getBestCmap()
lines = [] lines = []
for name in sorted(glyf.keys()):
for char in "abcdefghijklmnopqrstuvwxyz":
code = ord(char)
if code not in cmap:
continue
name = cmap[code]
g = glyf[name] g = glyf[name]
if not g.numberOfContours or g.numberOfContours <= 0: if not g.numberOfContours or g.numberOfContours <= 0:
continue continue
coords = g.coordinates coords = g.coordinates
ys = set(c[1] for c in coords) ys = set(c[1] for c in coords)
for zone_y, tolerance, inset, direction in zones: if 0 in ys:
# Glyph must have a point near the blue zone edge continue # has serif baseline
has_edge = any(abs(y - zone_y) <= tolerance for y in ys) bottom_pts = [i for i, (x, y) in enumerate(coords) if y <= 0]
if not has_edge: if not bottom_pts:
continue continue
for i, (x, y) in enumerate(coords): pts_str = ", ".join(str(p) for p in bottom_pts)
if direction == "left" and 0 < y <= inset: for shift_px, ppem_min, ppem_max in BASELINE_HINT_SHIFTS:
lines.append(f"{name} left {i}") shift = -abs(shift_px)
elif direction == "right" and zone_y - inset <= y < zone_y - tolerance: lines.append(
lines.append(f"{name} right {i}") f"{name} touch {pts_str} yshift {shift:.3f} @ {ppem_min}-{ppem_max}"
)
font.close() font.close()
return "\n".join(lines) return "\n".join(lines)
@@ -821,15 +854,15 @@ def autohint_ttf(ttf_path):
The resulting bytecode is baked into the font, so FreeType uses The resulting bytecode is baked into the font, so FreeType uses
the TrueType interpreter instead of falling back to auto-hinting. the TrueType interpreter instead of falling back to auto-hinting.
Additionally generates per-font control instructions for serif Additionally generates per-font touch deltas to deepen the
shelf points that the auto-hinter would otherwise interpolate. baseline anti-aliasing of non-serifed glyphs.
""" """
if not shutil.which("ttfautohint"): if not shutil.which("ttfautohint"):
print(" [warn] ttfautohint not found, skipping", file=sys.stderr) print(" [warn] ttfautohint not found, skipping", file=sys.stderr)
return return
# Generate control instructions for this specific font's points # Generate control instructions for this specific font's points
ctrl_text = _generate_serif_ctrl(ttf_path) ctrl_text = _generate_baseline_shift_ctrl(ttf_path)
ctrl_path = ttf_path + ".ctrl.tmp" ctrl_path = ttf_path + ".ctrl.tmp"
ctrl_count = 0 ctrl_count = 0
opts = list(AUTOHINT_OPTS) opts = list(AUTOHINT_OPTS)
@@ -1064,6 +1097,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, outline_fix=True):
clean_ttf_degenerate_contours(ttf_path) clean_ttf_degenerate_contours(ttf_path)
fix_ttf_style_flags(ttf_path, style_suffix) fix_ttf_style_flags(ttf_path, style_suffix)
add_kern_pairs(ttf_path) add_kern_pairs(ttf_path)
apply_glyph_y_ceiling(ttf_path)
autohint_ttf(ttf_path) autohint_ttf(ttf_path)

BIN
screenshot.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 192 KiB