|
8 | 8 | 2. Applies vertical scale (scale.py) via FontForge |
9 | 9 | 3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py) |
10 | 10 | 4. Exports to TTF → ./out/ttf/ |
11 | | - 5. Post-processes TTFs: x-height overshoot clamping, style flags, kern pairs, autohinting |
| 11 | + 5. Post-processes TTFs: style flags, kern pairs, autohinting |
12 | 12 |
|
13 | 13 | Uses FontForge (detected automatically). |
14 | 14 | Run with: python3 build.py |
|
43 | 43 | SRC_DIR = os.path.join(ROOT_DIR, "src") |
44 | 44 | OUT_DIR = os.path.join(ROOT_DIR, "out") |
45 | 45 | OUT_TTF_DIR = os.path.join(OUT_DIR, "ttf") # generated TTFs |
| 46 | +OUT_KF_DIR = os.path.join(OUT_DIR, "kf") # Kobo (KF) variants |
46 | 47 |
|
47 | 48 | REGULAR_VF = os.path.join(SRC_DIR, "Newsreader-VariableFont_opsz,wght.ttf") |
48 | 49 | ITALIC_VF = os.path.join(SRC_DIR, "Newsreader-Italic-VariableFont_opsz,wght.ttf") |
|
95 | 96 | # - Other options are left at ttfautohint defaults; uncomment to override. |
96 | 97 | AUTOHINT_OPTS = [ |
97 | 98 | "--no-info", |
98 | | - "--stem-width-mode=nss", |
| 99 | + "--stem-width-mode=qss", |
99 | 100 | # "--hinting-range-min=8", |
100 | 101 | # "--hinting-range-max=50", |
101 | 102 | # "--hinting-limit=200", |
102 | | - "--increase-x-height=0", |
| 103 | + "--increase-x-height=14", |
103 | 104 | ] |
104 | 105 |
|
105 | | -# Glyphs whose x-height overshoot is an outlier (+12 vs the standard +22). |
106 | | -# The inconsistent overshoot lands between the hinter's snap zones, causing |
107 | | -# these glyphs to render taller than their neighbors on low-res e-ink. |
108 | | -CLAMP_XHEIGHT_GLYPHS = ["u", "uogonek"] |
109 | | - |
110 | 106 | # Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units). |
111 | 107 | # Negative values tighten spacing. These are added on top of any existing |
112 | 108 | # kerning from the source variable font. |
@@ -610,61 +606,6 @@ def clean_ttf_degenerate_contours(ttf_path): |
610 | 606 | font.close() |
611 | 607 |
|
612 | 608 |
|
613 | | -def clamp_xheight_overshoot(ttf_path): |
614 | | - """Clamp outlier x-height overshoots in a TTF in-place. |
615 | | -
|
616 | | - Some glyphs (e.g. 'u') have a smaller overshoot than the standard |
617 | | - round overshoot, landing between the hinter's snap zones. This |
618 | | - flattens them to the true x-height measured from flat-topped glyphs. |
619 | | - """ |
620 | | - try: |
621 | | - from fontTools.ttLib import TTFont |
622 | | - except Exception: |
623 | | - print(" [warn] Skipping x-height clamp: fontTools not available", file=sys.stderr) |
624 | | - return |
625 | | - |
626 | | - font = TTFont(ttf_path) |
627 | | - glyf = font["glyf"] |
628 | | - |
629 | | - # Measure x-height from flat-topped reference glyphs. |
630 | | - xheight = 0 |
631 | | - for ref in ("x", "v"): |
632 | | - if ref not in glyf: |
633 | | - continue |
634 | | - coords = glyf[ref].coordinates |
635 | | - if coords: |
636 | | - ymax = max(c[1] for c in coords) |
637 | | - if ymax > xheight: |
638 | | - xheight = ymax |
639 | | - |
640 | | - if xheight == 0: |
641 | | - font.close() |
642 | | - return |
643 | | - |
644 | | - clamped = [] |
645 | | - for name in CLAMP_XHEIGHT_GLYPHS: |
646 | | - if name not in glyf: |
647 | | - continue |
648 | | - glyph = glyf[name] |
649 | | - coords = glyph.coordinates |
650 | | - if not coords: |
651 | | - continue |
652 | | - ymax = max(c[1] for c in coords) |
653 | | - if ymax <= xheight: |
654 | | - continue |
655 | | - glyph.coordinates = type(coords)( |
656 | | - [(x, min(y, xheight)) for x, y in coords] |
657 | | - ) |
658 | | - glyph_set = font.getGlyphSet() |
659 | | - if hasattr(glyph, "recalcBounds"): |
660 | | - glyph.recalcBounds(glyph_set) |
661 | | - clamped.append(name) |
662 | | - |
663 | | - if clamped: |
664 | | - font.save(ttf_path) |
665 | | - print(f" Clamped x-height overshoot for: {', '.join(clamped)} (xh={xheight})") |
666 | | - font.close() |
667 | | - |
668 | 609 |
|
669 | 610 | def fix_ttf_style_flags(ttf_path, style_suffix): |
670 | 611 | """Normalize OS/2 fsSelection and head.macStyle for style linking.""" |
@@ -867,6 +808,42 @@ def check_ttfautohint(): |
867 | 808 | sys.exit(1) |
868 | 809 |
|
869 | 810 |
|
| 811 | +KOBOFIX_URL = ( |
| 812 | + "https://raw.githubusercontent.com/nicoverbruggen/kobo-font-fix/main/kobofix.py" |
| 813 | +) |
| 814 | + |
| 815 | + |
| 816 | +def _download_kobofix(dest): |
| 817 | + """Download kobofix.py if not already cached.""" |
| 818 | + import urllib.request |
| 819 | + print(f" Downloading kobofix.py ...") |
| 820 | + urllib.request.urlretrieve(KOBOFIX_URL, dest) |
| 821 | + print(f" Saved to {dest}") |
| 822 | + |
| 823 | + |
| 824 | +def _run_kobofix(kobofix_path, variant_names): |
| 825 | + """Run kobofix.py --preset kf on built TTFs, move KF_ files to out/kf/.""" |
| 826 | + ttf_files = [os.path.join(OUT_TTF_DIR, f"{n}.ttf") for n in variant_names] |
| 827 | + cmd = [sys.executable, kobofix_path, "--preset", "kf"] + ttf_files |
| 828 | + result = subprocess.run(cmd, capture_output=True, text=True) |
| 829 | + if result.stdout: |
| 830 | + print(result.stdout, end="") |
| 831 | + if result.returncode != 0: |
| 832 | + print("\nERROR: kobofix.py failed", file=sys.stderr) |
| 833 | + if result.stderr: |
| 834 | + print(result.stderr, file=sys.stderr) |
| 835 | + sys.exit(1) |
| 836 | + |
| 837 | + os.makedirs(OUT_KF_DIR, exist_ok=True) |
| 838 | + import glob |
| 839 | + moved = 0 |
| 840 | + for kf_file in glob.glob(os.path.join(OUT_TTF_DIR, "KF_*.ttf")): |
| 841 | + dest = os.path.join(OUT_KF_DIR, os.path.basename(kf_file)) |
| 842 | + shutil.move(kf_file, dest) |
| 843 | + moved += 1 |
| 844 | + print(f" Moved {moved} KF font(s) to {OUT_KF_DIR}/") |
| 845 | + |
| 846 | + |
870 | 847 | def main(): |
871 | 848 | print("=" * 60) |
872 | 849 | print(" Readerly Build") |
@@ -998,15 +975,22 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, outline_fix=True): |
998 | 975 | run_fontforge_script(script) |
999 | 976 | if outline_fix: |
1000 | 977 | clean_ttf_degenerate_contours(ttf_path) |
1001 | | - clamp_xheight_overshoot(ttf_path) |
1002 | 978 | fix_ttf_style_flags(ttf_path, style_suffix) |
1003 | 979 | add_kern_pairs(ttf_path) |
1004 | 980 | autohint_ttf(ttf_path) |
1005 | 981 |
|
1006 | 982 |
|
| 983 | + # Step 5: Generate Kobo (KF) variants via kobofix.py |
| 984 | + print("\n── Step 5: Generate Kobo (KF) variants ──\n") |
| 985 | + |
| 986 | + kobofix_path = os.path.join(tmp_dir, "kobofix.py") |
| 987 | + _download_kobofix(kobofix_path) |
| 988 | + _run_kobofix(kobofix_path, variant_names) |
| 989 | + |
1007 | 990 | print("\n" + "=" * 60) |
1008 | 991 | print(" Build complete!") |
1009 | 992 | print(f" TTF fonts are in: {OUT_TTF_DIR}/") |
| 993 | + print(f" KF fonts are in: {OUT_KF_DIR}/") |
1010 | 994 | print("=" * 60) |
1011 | 995 |
|
1012 | 996 |
|
|
0 commit comments