diff --git a/README.md b/README.md index 8405561..8bc4a77 100644 --- a/README.md +++ b/README.md @@ -87,4 +87,12 @@ Several metadata scripts are applied via FontForge: #### Step 4: Export -The final fonts are exported from FontForge as both SFD (FontForge source) and TTF. The build supports optional old-style kern tables, but this is off by default because it has no effect on device tests. +The final fonts are exported from FontForge as TTF. A cleanup step removes zero-area contours that can cause missing glyphs on macOS. The build supports optional old-style kern tables, but this is off by default because it has no effect on device tests. + +#### TTF cleanup (manual exports) + +Some FontForge exports emit 1–2 point contours in the `glyf` table. macOS can treat these as invalid and skip the glyph entirely (for example, `m` or italic `u`). The build pipeline removes these zero-area contours automatically. If you manually export a TTF from an SFD and see missing glyphs, run: + +``` +python3 cleanup_ttf.py out/ttf/Readerly-Regular.ttf +``` diff --git a/build.py b/build.py index 482c485..4631a85 100755 --- a/build.py +++ b/build.py @@ -179,7 +179,7 @@ def build_export_script(sfd_path, ttf_path, old_kern=True): def clean_ttf_degenerate_contours(ttf_path): - """Remove degenerate contours (<=2 points) from a TTF in-place.""" + """Remove zero-area contours (<=2 points) from a TTF in-place.""" try: from fontTools.ttLib import TTFont from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates @@ -236,7 +236,7 @@ def clean_ttf_degenerate_contours(ttf_path): if hasattr(glyf, "recalcBounds"): glyf.recalcBounds(glyph_set) # type: ignore[attr-defined] font.save(ttf_path) - print(f" Cleaned {removed_total} degenerate contour(s)") + print(f" Cleaned {removed_total} zero-area contour(s)") font.close() @@ -305,6 +305,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True): print(result.stderr, file=sys.stderr) sys.exit(1) + print(f" {len(variants)} font(s) instanced.") # Step 2: Apply vertical scale (opens TTF, saves as SFD) @@ -374,6 +375,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True): run_fontforge_script(script) clean_ttf_degenerate_contours(ttf_path) + print("\n" + "=" * 60) print(" Build complete!") print(f" SFD fonts are in: {OUT_SFD_DIR}/") diff --git a/cleanup_ttf.py b/cleanup_ttf.py new file mode 100644 index 0000000..9c910f2 --- /dev/null +++ b/cleanup_ttf.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Remove zero-area contours from a TTF. + +Some FontForge exports emit 1–2 point contours that macOS can treat as +invalid and skip the glyph entirely. This script removes those contours +in-place. +""" + +import sys + + +def clean_ttf_degenerate_contours(ttf_path): + try: + from fontTools.ttLib import TTFont + from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates + except Exception as exc: + raise SystemExit(f"ERROR: fontTools is required ({exc})") + + font = TTFont(ttf_path) + glyf = font["glyf"] # type: ignore[index] + + removed_total = 0 + modified = set() + for name in font.getGlyphOrder(): + glyph = glyf[name] # type: ignore[index] + if glyph.isComposite(): + continue + end_pts = getattr(glyph, "endPtsOfContours", None) + if not end_pts: + continue + + coords = glyph.coordinates + flags = glyph.flags + + new_coords = [] + new_flags = [] + new_end_pts = [] + + start = 0 + removed = 0 + for end in end_pts: + count = end - start + 1 + if count <= 2: + removed += 1 + else: + new_coords.extend(coords[start:end + 1]) + new_flags.extend(flags[start:end + 1]) + new_end_pts.append(len(new_coords) - 1) + start = end + 1 + + if removed: + removed_total += removed + modified.add(name) + glyph.coordinates = GlyphCoordinates(new_coords) + glyph.flags = new_flags + glyph.endPtsOfContours = new_end_pts + glyph.numberOfContours = len(new_end_pts) + + if removed_total: + glyph_set = font.getGlyphSet() + for name in modified: + glyph = glyf[name] # type: ignore[index] + if hasattr(glyph, "recalcBounds"): + glyph.recalcBounds(glyph_set) + if hasattr(glyf, "recalcBounds"): + glyf.recalcBounds(glyph_set) # type: ignore[attr-defined] + font.save(ttf_path) + + font.close() + return removed_total + + +def main(): + if len(sys.argv) != 2: + raise SystemExit("Usage: python3 cleanup_ttf.py path/to/font.ttf") + ttf_path = sys.argv[1] + removed = clean_ttf_degenerate_contours(ttf_path) + print(f"Cleaned {removed} zero-area contour(s): {ttf_path}") + + +if __name__ == "__main__": + main()