9090ASCENDER_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.
9796AUTOHINT_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
0 commit comments