Skip to content

Commit e3b8fea

Browse files
Update hinting again
1 parent 83a84b7 commit e3b8fea

2 files changed

Lines changed: 83 additions & 49 deletions

File tree

build.py

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,32 @@
9090
ASCENDER_RATIO = 0.8
9191

9292
# Step 4: ttfautohint options (hinting for Kobo's FreeType renderer)
93-
# - Kobo uses FreeType grayscale on e-ink, where gray pixels are very
94-
# visible. Strong mode (s) snaps stems to integer pixels, minimising
95-
# gray anti-aliasing at the cost of some shape distortion.
96-
# - Other options are left at ttfautohint defaults.
93+
# - Kobo uses FreeType grayscale, so the 1st char of --stem-width-mode
94+
# matters: n=natural (least distortion), q=quantized, s=strong.
95+
# - x-height snapping is disabled to avoid inconsistent glyph heights.
9796
AUTOHINT_OPTS = [
9897
"--no-info",
99-
"--stem-width-mode=qss",
98+
"--stem-width-mode=nss",
99+
"--increase-x-height=0",
100+
'--x-height-snapping-exceptions=-',
100101
]
101102

102-
# Serif shelf detection: points within this distance (in font units)
103-
# inward from a blue zone edge get `left`/`right` direction hints so
104-
# ttfautohint creates explicit segments instead of interpolating them.
105-
# This fixes gray ghosting at serif feet (bottom) and tops (e.g. w, v).
106-
SERIF_SHELF_INSET = 160 # how far the shelf can be from the blue zone
103+
# Baseline alignment: deepen the bottom anti-aliasing of non-serifed
104+
# glyphs via hinting-only touch deltas (no outline changes). This
105+
# shifts their bottom points down during rasterization so they produce
106+
# more gray below the baseline, visually matching serifed characters.
107+
# Each entry is (shift_px, ppem_min, ppem_max). Shifts are in pixels
108+
# (multiples of 1/8, max 1.0). Set to empty list to disable.
109+
BASELINE_HINT_SHIFTS = [
110+
(0.125, 6, 53),
111+
]
112+
113+
# Per-glyph Y ceiling: cap the top of specific glyphs to reduce
114+
# oversized or awkward serifs. (glyph_name, max_y)
115+
# Points above max_y are clamped down to max_y.
116+
GLYPH_Y_CEILING = [
117+
("u", 1062), # flatten tiny top serif tips to platform level
118+
]
107119

108120
# Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units).
109121
# Negative values tighten spacing. These are added on top of any existing
@@ -754,54 +766,75 @@ def resolve(name):
754766
print(f" Added {count} kern pair(s) to GPOS")
755767

756768

757-
def _generate_serif_ctrl(ttf_path):
758-
"""Generate ttfautohint control instructions for serif shelf points.
759769

760-
Scans the font for glyph points near (but not on) blue zone edges
761-
— the serif "shelves" that ttfautohint doesn't detect as segments.
762-
Without explicit hints these get interpolated to fractional pixel
763-
positions, causing gray ghosting on e-ink.
770+
def apply_glyph_y_ceiling(ttf_path):
771+
"""Clamp glyph points above a Y ceiling down to the ceiling value."""
772+
if not GLYPH_Y_CEILING:
773+
return
764774

765-
Bottom shelves (near baseline y=0) get `left` direction hints.
766-
Top shelves (near x-height) get `right` direction hints.
767-
"""
768775
from fontTools.ttLib import TTFont
769776
font = TTFont(ttf_path)
770777
glyf = font["glyf"]
771-
os2 = font["OS/2"]
772-
x_height = getattr(os2, "sxHeight", None) or 0
773-
774-
# Blue zone edges: (zone_y, tolerance, inset, direction)
775-
# - tolerance: how close a glyph's "flat edge" must be to zone_y
776-
# - inset: how far inward the shelf can be from the flat edge
777-
# - direction: left = bottom shelf, right = top shelf
778-
zones = [
779-
# (zone_y, tolerance, inset, direction)
780-
# tolerance: how close a glyph's flat edge must be to count
781-
# inset: how far the shelf can be from the zone
782-
# Shelf points are between zone_y±inset and zone_y±tolerance.
783-
(0, 0, SERIF_SHELF_INSET, "left"),
784-
]
785-
if x_height:
786-
zones.append((x_height, 25, SERIF_SHELF_INSET, "right"))
778+
modified = []
779+
780+
for glyph_name, max_y in GLYPH_Y_CEILING:
781+
g = glyf.get(glyph_name)
782+
if not g or not g.numberOfContours or g.numberOfContours <= 0:
783+
continue
784+
coords = g.coordinates
785+
clamped = 0
786+
for j in range(len(coords)):
787+
if coords[j][1] > max_y:
788+
coords[j] = (coords[j][0], max_y)
789+
clamped += 1
790+
if clamped:
791+
modified.append(f"{glyph_name}({clamped}pts)")
792+
793+
if modified:
794+
font.save(ttf_path)
795+
print(f" Clamped Y ceiling: {', '.join(modified)}")
796+
font.close()
797+
787798

799+
def _generate_baseline_shift_ctrl(ttf_path):
800+
"""Generate touch deltas to deepen bottom anti-aliasing of non-serifed glyphs.
801+
802+
For lowercase glyphs without a flat baseline (no serif foot), shifts
803+
the bottom-most points down during rasterization. Uses graduated
804+
shifts from BASELINE_HINT_SHIFTS — stronger at small ppem sizes
805+
where alignment is most noticeable. No outline changes.
806+
"""
807+
if not BASELINE_HINT_SHIFTS:
808+
return ""
809+
810+
from fontTools.ttLib import TTFont
811+
font = TTFont(ttf_path)
812+
glyf = font["glyf"]
813+
cmap = font.getBestCmap()
788814
lines = []
789-
for name in sorted(glyf.keys()):
815+
816+
for char in "abcdefghijklmnopqrstuvwxyz":
817+
code = ord(char)
818+
if code not in cmap:
819+
continue
820+
name = cmap[code]
790821
g = glyf[name]
791822
if not g.numberOfContours or g.numberOfContours <= 0:
792823
continue
793824
coords = g.coordinates
794825
ys = set(c[1] for c in coords)
795-
for zone_y, tolerance, inset, direction in zones:
796-
# Glyph must have a point near the blue zone edge
797-
has_edge = any(abs(y - zone_y) <= tolerance for y in ys)
798-
if not has_edge:
799-
continue
800-
for i, (x, y) in enumerate(coords):
801-
if direction == "left" and 0 < y <= inset:
802-
lines.append(f"{name} left {i}")
803-
elif direction == "right" and zone_y - inset <= y < zone_y - tolerance:
804-
lines.append(f"{name} right {i}")
826+
if 0 in ys:
827+
continue # has serif baseline
828+
bottom_pts = [i for i, (x, y) in enumerate(coords) if y <= 0]
829+
if not bottom_pts:
830+
continue
831+
pts_str = ", ".join(str(p) for p in bottom_pts)
832+
for shift_px, ppem_min, ppem_max in BASELINE_HINT_SHIFTS:
833+
shift = -abs(shift_px)
834+
lines.append(
835+
f"{name} touch {pts_str} yshift {shift:.3f} @ {ppem_min}-{ppem_max}"
836+
)
837+
805838
font.close()
806839
return "\n".join(lines)
807840

@@ -821,15 +854,15 @@ def autohint_ttf(ttf_path):
821854
The resulting bytecode is baked into the font, so FreeType uses
822855
the TrueType interpreter instead of falling back to auto-hinting.
823856
824-
Additionally generates per-font control instructions for serif
825-
shelf points that the auto-hinter would otherwise interpolate.
857+
Additionally generates per-font touch deltas to deepen the
858+
baseline anti-aliasing of non-serifed glyphs.
826859
"""
827860
if not shutil.which("ttfautohint"):
828861
print(" [warn] ttfautohint not found, skipping", file=sys.stderr)
829862
return
830863

831864
# Generate control instructions for this specific font's points
832-
ctrl_text = _generate_serif_ctrl(ttf_path)
865+
ctrl_text = _generate_baseline_shift_ctrl(ttf_path)
833866
ctrl_path = ttf_path + ".ctrl.tmp"
834867
ctrl_count = 0
835868
opts = list(AUTOHINT_OPTS)
@@ -1064,6 +1097,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, outline_fix=True):
10641097
clean_ttf_degenerate_contours(ttf_path)
10651098
fix_ttf_style_flags(ttf_path, style_suffix)
10661099
add_kern_pairs(ttf_path)
1100+
apply_glyph_y_ceiling(ttf_path)
10671101
autohint_ttf(ttf_path)
10681102

10691103

screenshot.png

100755100644
113 KB
Loading

0 commit comments

Comments
 (0)