Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3b8fea2a5 | |||
| 83a84b7941 | |||
| 19e915f4e1 |
143
build.py
143
build.py
@@ -91,16 +91,30 @@ ASCENDER_RATIO = 0.8
|
|||||||
|
|
||||||
# Step 4: ttfautohint options (hinting for Kobo's FreeType renderer)
|
# Step 4: ttfautohint options (hinting for Kobo's FreeType renderer)
|
||||||
# - Kobo uses FreeType grayscale, so the 1st char of --stem-width-mode
|
# - Kobo uses FreeType grayscale, so the 1st char of --stem-width-mode
|
||||||
# (gray) is the one that matters. n=natural, q=quantized, s=strong.
|
# matters: n=natural (least distortion), q=quantized, s=strong.
|
||||||
# - Remaining two chars are for GDI and DirectWrite (not used on Kobo).
|
# - x-height snapping is disabled to avoid inconsistent glyph heights.
|
||||||
# - Other options are left at ttfautohint defaults; uncomment to override.
|
|
||||||
AUTOHINT_OPTS = [
|
AUTOHINT_OPTS = [
|
||||||
"--no-info",
|
"--no-info",
|
||||||
"--stem-width-mode=qss",
|
"--stem-width-mode=nss",
|
||||||
# "--hinting-range-min=8",
|
"--increase-x-height=0",
|
||||||
# "--hinting-range-max=50",
|
'--x-height-snapping-exceptions=-',
|
||||||
# "--hinting-limit=200",
|
]
|
||||||
"--increase-x-height=14",
|
|
||||||
|
# Baseline alignment: deepen the bottom anti-aliasing of non-serifed
|
||||||
|
# glyphs via hinting-only touch deltas (no outline changes). This
|
||||||
|
# shifts their bottom points down during rasterization so they produce
|
||||||
|
# more gray below the baseline, visually matching serifed characters.
|
||||||
|
# Each entry is (shift_px, ppem_min, ppem_max). Shifts are in pixels
|
||||||
|
# (multiples of 1/8, max 1.0). Set to empty list to disable.
|
||||||
|
BASELINE_HINT_SHIFTS = [
|
||||||
|
(0.125, 6, 53),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Per-glyph Y ceiling: cap the top of specific glyphs to reduce
|
||||||
|
# oversized or awkward serifs. (glyph_name, max_y)
|
||||||
|
# Points above max_y are clamped down to max_y.
|
||||||
|
GLYPH_Y_CEILING = [
|
||||||
|
("u", 1062), # flatten tiny top serif tips to platform level
|
||||||
]
|
]
|
||||||
|
|
||||||
# Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units).
|
# Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units).
|
||||||
@@ -752,6 +766,79 @@ def add_kern_pairs(ttf_path):
|
|||||||
print(f" Added {count} kern pair(s) to GPOS")
|
print(f" Added {count} kern pair(s) to GPOS")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def apply_glyph_y_ceiling(ttf_path):
|
||||||
|
"""Clamp glyph points above a Y ceiling down to the ceiling value."""
|
||||||
|
if not GLYPH_Y_CEILING:
|
||||||
|
return
|
||||||
|
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
font = TTFont(ttf_path)
|
||||||
|
glyf = font["glyf"]
|
||||||
|
modified = []
|
||||||
|
|
||||||
|
for glyph_name, max_y in GLYPH_Y_CEILING:
|
||||||
|
g = glyf.get(glyph_name)
|
||||||
|
if not g or not g.numberOfContours or g.numberOfContours <= 0:
|
||||||
|
continue
|
||||||
|
coords = g.coordinates
|
||||||
|
clamped = 0
|
||||||
|
for j in range(len(coords)):
|
||||||
|
if coords[j][1] > max_y:
|
||||||
|
coords[j] = (coords[j][0], max_y)
|
||||||
|
clamped += 1
|
||||||
|
if clamped:
|
||||||
|
modified.append(f"{glyph_name}({clamped}pts)")
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
font.save(ttf_path)
|
||||||
|
print(f" Clamped Y ceiling: {', '.join(modified)}")
|
||||||
|
font.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_baseline_shift_ctrl(ttf_path):
|
||||||
|
"""Generate touch deltas to deepen bottom anti-aliasing of non-serifed glyphs.
|
||||||
|
|
||||||
|
For lowercase glyphs without a flat baseline (no serif foot), shifts
|
||||||
|
the bottom-most points down during rasterization. Uses graduated
|
||||||
|
shifts from BASELINE_HINT_SHIFTS — stronger at small ppem sizes
|
||||||
|
where alignment is most noticeable. No outline changes.
|
||||||
|
"""
|
||||||
|
if not BASELINE_HINT_SHIFTS:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
font = TTFont(ttf_path)
|
||||||
|
glyf = font["glyf"]
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for char in "abcdefghijklmnopqrstuvwxyz":
|
||||||
|
code = ord(char)
|
||||||
|
if code not in cmap:
|
||||||
|
continue
|
||||||
|
name = cmap[code]
|
||||||
|
g = glyf[name]
|
||||||
|
if not g.numberOfContours or g.numberOfContours <= 0:
|
||||||
|
continue
|
||||||
|
coords = g.coordinates
|
||||||
|
ys = set(c[1] for c in coords)
|
||||||
|
if 0 in ys:
|
||||||
|
continue # has serif baseline
|
||||||
|
bottom_pts = [i for i, (x, y) in enumerate(coords) if y <= 0]
|
||||||
|
if not bottom_pts:
|
||||||
|
continue
|
||||||
|
pts_str = ", ".join(str(p) for p in bottom_pts)
|
||||||
|
for shift_px, ppem_min, ppem_max in BASELINE_HINT_SHIFTS:
|
||||||
|
shift = -abs(shift_px)
|
||||||
|
lines.append(
|
||||||
|
f"{name} touch {pts_str} yshift {shift:.3f} @ {ppem_min}-{ppem_max}"
|
||||||
|
)
|
||||||
|
|
||||||
|
font.close()
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def autohint_ttf(ttf_path):
|
def autohint_ttf(ttf_path):
|
||||||
"""Run ttfautohint to add proper TrueType hinting.
|
"""Run ttfautohint to add proper TrueType hinting.
|
||||||
|
|
||||||
@@ -766,16 +853,34 @@ def autohint_ttf(ttf_path):
|
|||||||
hinting, which may handle sub-baseline overshoots more gracefully.
|
hinting, which may handle sub-baseline overshoots more gracefully.
|
||||||
The resulting bytecode is baked into the font, so FreeType uses
|
The resulting bytecode is baked into the font, so FreeType uses
|
||||||
the TrueType interpreter instead of falling back to auto-hinting.
|
the TrueType interpreter instead of falling back to auto-hinting.
|
||||||
|
|
||||||
|
Additionally generates per-font touch deltas to deepen the
|
||||||
|
baseline anti-aliasing of non-serifed glyphs.
|
||||||
"""
|
"""
|
||||||
if not shutil.which("ttfautohint"):
|
if not shutil.which("ttfautohint"):
|
||||||
print(" [warn] ttfautohint not found, skipping", file=sys.stderr)
|
print(" [warn] ttfautohint not found, skipping", file=sys.stderr)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Generate control instructions for this specific font's points
|
||||||
|
ctrl_text = _generate_baseline_shift_ctrl(ttf_path)
|
||||||
|
ctrl_path = ttf_path + ".ctrl.tmp"
|
||||||
|
ctrl_count = 0
|
||||||
|
opts = list(AUTOHINT_OPTS)
|
||||||
|
if ctrl_text:
|
||||||
|
with open(ctrl_path, "w") as f:
|
||||||
|
f.write(ctrl_text)
|
||||||
|
opts += [f"--control-file={ctrl_path}"]
|
||||||
|
ctrl_count = ctrl_text.count("\n") + 1
|
||||||
|
|
||||||
tmp_path = ttf_path + ".autohint.tmp"
|
tmp_path = ttf_path + ".autohint.tmp"
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["ttfautohint"] + AUTOHINT_OPTS + [ttf_path, tmp_path],
|
["ttfautohint"] + opts + [ttf_path, tmp_path],
|
||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if os.path.exists(ctrl_path):
|
||||||
|
os.remove(ctrl_path)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f" [warn] ttfautohint failed: {result.stderr.strip()}", file=sys.stderr)
|
print(f" [warn] ttfautohint failed: {result.stderr.strip()}", file=sys.stderr)
|
||||||
if os.path.exists(tmp_path):
|
if os.path.exists(tmp_path):
|
||||||
@@ -783,7 +888,10 @@ def autohint_ttf(ttf_path):
|
|||||||
return
|
return
|
||||||
|
|
||||||
os.replace(tmp_path, ttf_path)
|
os.replace(tmp_path, ttf_path)
|
||||||
print(f" Autohinted with ttfautohint")
|
hint_msg = "Autohinted with ttfautohint"
|
||||||
|
if ctrl_count:
|
||||||
|
hint_msg += f" ({ctrl_count} serif control hints)"
|
||||||
|
print(f" {hint_msg}")
|
||||||
|
|
||||||
|
|
||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@@ -815,6 +923,9 @@ KOBOFIX_URL = (
|
|||||||
|
|
||||||
def _download_kobofix(dest):
|
def _download_kobofix(dest):
|
||||||
"""Download kobofix.py if not already cached."""
|
"""Download kobofix.py if not already cached."""
|
||||||
|
if os.path.isfile(dest):
|
||||||
|
print(f" Using cached kobofix.py")
|
||||||
|
return
|
||||||
import urllib.request
|
import urllib.request
|
||||||
print(f" Downloading kobofix.py ...")
|
print(f" Downloading kobofix.py ...")
|
||||||
urllib.request.urlretrieve(KOBOFIX_URL, dest)
|
urllib.request.urlretrieve(KOBOFIX_URL, dest)
|
||||||
@@ -857,9 +968,18 @@ def main():
|
|||||||
family = DEFAULT_FAMILY
|
family = DEFAULT_FAMILY
|
||||||
outline_fix = True
|
outline_fix = True
|
||||||
|
|
||||||
|
# --name "Foo" sets the family name directly
|
||||||
|
if "--name" in sys.argv:
|
||||||
|
idx = sys.argv.index("--name")
|
||||||
|
if idx + 1 < len(sys.argv):
|
||||||
|
family = sys.argv[idx + 1]
|
||||||
|
else:
|
||||||
|
print("ERROR: --name requires a value", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if "--customize" in sys.argv:
|
if "--customize" in sys.argv:
|
||||||
print()
|
print()
|
||||||
family = input(f" Font family name [{DEFAULT_FAMILY}]: ").strip() or DEFAULT_FAMILY
|
family = input(f" Font family name [{family}]: ").strip() or family
|
||||||
outline_input = input(" Apply outline fixes (remove overlaps + zero-area cleanup)? [Y/n]: ").strip().lower()
|
outline_input = input(" Apply outline fixes (remove overlaps + zero-area cleanup)? [Y/n]: ").strip().lower()
|
||||||
outline_fix = outline_input not in ("n", "no")
|
outline_fix = outline_input not in ("n", "no")
|
||||||
|
|
||||||
@@ -977,6 +1097,7 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, outline_fix=True):
|
|||||||
clean_ttf_degenerate_contours(ttf_path)
|
clean_ttf_degenerate_contours(ttf_path)
|
||||||
fix_ttf_style_flags(ttf_path, style_suffix)
|
fix_ttf_style_flags(ttf_path, style_suffix)
|
||||||
add_kern_pairs(ttf_path)
|
add_kern_pairs(ttf_path)
|
||||||
|
apply_glyph_y_ceiling(ttf_path)
|
||||||
autohint_ttf(ttf_path)
|
autohint_ttf(ttf_path)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
BIN
screenshot.png
Executable file → Normal file
BIN
screenshot.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 192 KiB |
Reference in New Issue
Block a user