Fix zero-area contours after export to TTF
This commit is contained in:
10
README.md
10
README.md
@@ -87,4 +87,12 @@ Several metadata scripts are applied via FontForge:
|
|||||||
|
|
||||||
#### Step 4: Export
|
#### 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
|
||||||
|
```
|
||||||
|
|||||||
6
build.py
6
build.py
@@ -179,7 +179,7 @@ def build_export_script(sfd_path, ttf_path, old_kern=True):
|
|||||||
|
|
||||||
|
|
||||||
def clean_ttf_degenerate_contours(ttf_path):
|
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:
|
try:
|
||||||
from fontTools.ttLib import TTFont
|
from fontTools.ttLib import TTFont
|
||||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
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"):
|
if hasattr(glyf, "recalcBounds"):
|
||||||
glyf.recalcBounds(glyph_set) # type: ignore[attr-defined]
|
glyf.recalcBounds(glyph_set) # type: ignore[attr-defined]
|
||||||
font.save(ttf_path)
|
font.save(ttf_path)
|
||||||
print(f" Cleaned {removed_total} degenerate contour(s)")
|
print(f" Cleaned {removed_total} zero-area contour(s)")
|
||||||
font.close()
|
font.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -305,6 +305,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True):
|
|||||||
print(result.stderr, file=sys.stderr)
|
print(result.stderr, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
print(f" {len(variants)} font(s) instanced.")
|
print(f" {len(variants)} font(s) instanced.")
|
||||||
|
|
||||||
# Step 2: Apply vertical scale (opens TTF, saves as SFD)
|
# 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)
|
run_fontforge_script(script)
|
||||||
clean_ttf_degenerate_contours(ttf_path)
|
clean_ttf_degenerate_contours(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" SFD fonts are in: {OUT_SFD_DIR}/")
|
||||||
|
|||||||
83
cleanup_ttf.py
Normal file
83
cleanup_ttf.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user