1
0

Use variable fonts as source of truth, tweaks

This commit is contained in:
2026-03-02 03:26:15 +01:00
parent 04374e77c5
commit cdcc0942c7
12 changed files with 171 additions and 66491 deletions

113
build.py
View File

@@ -4,9 +4,9 @@ Readerly Build Script
─────────────────────
Orchestrates the full font build pipeline:
1. Copies ./src/*.sfd → ./src_processed/
2. Applies vertical scale (scale.py)
3. Applies vertical metrics (metrics.py)
1. Instances variable fonts into static TTFs (fontTools.instancer)
2. Applies vertical scale (scale.py) via FontForge
3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py)
4. Exports to TTF with old-style kern table → ./out/
Uses the Flatpak version of FontForge.
@@ -31,6 +31,21 @@ SCRIPTS_DIR = os.path.join(ROOT_DIR, "scripts")
FLATPAK_APP = "org.fontforge.FontForge"
REGULAR_VF = os.path.join(SRC_DIR, "Newsreader-VariableFont_opsz,wght.ttf")
ITALIC_VF = os.path.join(SRC_DIR, "Newsreader-Italic-VariableFont_opsz,wght.ttf")
VARIANTS = [
# (output_name, source_vf, wght, opsz)
("Readerly-Regular", REGULAR_VF, 430, 9),
("Readerly-Bold", REGULAR_VF, 550, 9),
("Readerly-Italic", ITALIC_VF, 430, 9),
("Readerly-BoldItalic", ITALIC_VF, 550, 9),
]
# Glyphs to clear — stacked diacritics that inflate head.yMax far beyond
# the design ascender. Aringacute (Ǻ/ǻ) is the sole outlier at 2268 units.
CLEAR_GLYPHS = ["Aringacute", "aringacute"]
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# HELPERS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -63,24 +78,25 @@ def run_fontforge_script(script_text):
sys.exit(1)
def build_per_font_script(sfd_path, steps):
def build_per_font_script(open_path, save_path, steps):
"""
Build a FontForge Python script that opens an .sfd file, runs the given
step scripts (which expect `f` to be the active font), saves, and closes.
Build a FontForge Python script that opens a font file, runs the given
step scripts (which expect `f` to be the active font), saves as .sfd,
and closes.
Each step is a (label, script_body) tuple. The script_body should use `f`
as the font variable.
"""
parts = [
f'import fontforge',
f'f = fontforge.open({sfd_path!r})',
f'f = fontforge.open({open_path!r})',
f'print("\\nOpened: " + f.fontname + "\\n")',
]
for label, body in steps:
parts.append(f'print("── {label} ──\\n")')
parts.append(body)
parts.append(f'f.save({sfd_path!r})')
parts.append(f'print("\\nSaved: {sfd_path}\\n")')
parts.append(f'f.save({save_path!r})')
parts.append(f'print("\\nSaved: {save_path}\\n")')
parts.append('f.close()')
return "\n".join(parts)
@@ -123,43 +139,77 @@ def main():
print(" Readerly Build")
print("=" * 60)
# Step 1: Copy src → src_processed
print("\n── Step 1: Copy sources to ./src_processed ──\n")
# Step 1: Instance variable fonts into static TTFs
print("\n── Step 1: Instance variable fonts ──\n")
if os.path.exists(MUTATED_DIR):
shutil.rmtree(MUTATED_DIR)
shutil.copytree(SRC_DIR, MUTATED_DIR)
sfd_files = sorted(f for f in os.listdir(MUTATED_DIR) if f.endswith(".sfd"))
for f in sfd_files:
print(f" Copied: {f}")
print(f" {len(sfd_files)} font(s) ready.")
os.makedirs(MUTATED_DIR)
# Step 2: Apply vertical scale to lowercase glyphs
for name, vf_path, wght, opsz in VARIANTS:
ttf_out = os.path.join(MUTATED_DIR, f"{name}.ttf")
print(f" Instancing {name} (wght={wght}, opsz={opsz})")
cmd = [
sys.executable, "-m", "fontTools", "varLib.instancer",
vf_path,
"-o", ttf_out,
f"wght={wght}",
f"opsz={opsz}",
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.stdout:
print(result.stdout, end="")
if result.returncode != 0:
print(f"\nERROR: instancer failed for {name}", file=sys.stderr)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(1)
variant_names = [name for name, _, _, _ in VARIANTS]
print(f" {len(VARIANTS)} font(s) instanced.")
# Step 2: Apply vertical scale (opens TTF, saves as SFD)
print("\n── Step 2: Scale lowercase ──\n")
scale_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "scale.py"))
for sfd_name in sfd_files:
sfd_path = os.path.join(MUTATED_DIR, sfd_name)
print(f"Scaling: {sfd_name}")
clear_code = "\n".join(
f'if {g!r} in f:\n'
f' f[{g!r}].clear()\n'
f' print(" Cleared: {g}")'
for g in CLEAR_GLYPHS
)
script = build_per_font_script(sfd_path, [
for name in variant_names:
ttf_path = os.path.join(MUTATED_DIR, f"{name}.ttf")
sfd_path = os.path.join(MUTATED_DIR, f"{name}.sfd")
print(f"Scaling: {name}")
script = build_per_font_script(ttf_path, sfd_path, [
("Clearing problematic glyphs", clear_code),
("Scaling Y", scale_code),
])
run_fontforge_script(script)
# Step 3: Apply metrics and rename
# Step 3: Apply metrics and rename (opens SFD, saves as SFD)
print("\n── Step 3: Apply metrics and rename ──\n")
metrics_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "metrics.py"))
rename_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "rename.py"))
metrics_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "metrics.py"))
lineheight_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "lineheight.py"))
rename_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "rename.py"))
for sfd_name in sfd_files:
sfd_path = os.path.join(MUTATED_DIR, sfd_name)
print(f"Processing: {sfd_name}")
for name in variant_names:
sfd_path = os.path.join(MUTATED_DIR, f"{name}.sfd")
print(f"Processing: {name}")
print("-" * 40)
script = build_per_font_script(sfd_path, [
# Set fontname so rename.py can detect the correct style suffix
set_fontname = f'f.fontname = {name!r}'
script = build_per_font_script(sfd_path, sfd_path, [
("Setting vertical metrics", metrics_code),
("Adjusting line height", lineheight_code),
("Setting fontname for rename", set_fontname),
("Updating font names", rename_code),
])
run_fontforge_script(script)
@@ -168,10 +218,9 @@ def main():
print("\n── Step 4: Export to TTF ──\n")
os.makedirs(OUT_DIR, exist_ok=True)
for sfd_name in sfd_files:
sfd_path = os.path.join(MUTATED_DIR, sfd_name)
ttf_name = sfd_name.replace(".sfd", ".ttf")
ttf_path = os.path.join(OUT_DIR, ttf_name)
for name in variant_names:
sfd_path = os.path.join(MUTATED_DIR, f"{name}.sfd")
ttf_path = os.path.join(OUT_DIR, f"{name}.ttf")
script = build_export_script(sfd_path, ttf_path)
run_fontforge_script(script)