Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef39ce4046 | |||
| 5cfc4ea36f | |||
| efc9c37522 | |||
| f43edba38f | |||
| a736526056 | |||
| d4cce21701 | |||
| b96bb96a88 | |||
| 2a27486aca |
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@ name: Build fonts
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, develop]
|
||||||
tags: ["*"]
|
tags: ["*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -12,27 +12,37 @@ jobs:
|
|||||||
image: ghcr.io/nicoverbruggen/fntbld-oci:latest
|
image: ghcr.io/nicoverbruggen/fntbld-oci:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Build fonts
|
- name: Build fonts
|
||||||
run: python3 build.py
|
run: python3 build.py
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Readerly
|
name: Readerly
|
||||||
path: out/ttf/*.ttf
|
path: out/ttf/*.ttf
|
||||||
|
|
||||||
|
- name: Upload Kobo artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: KF_Readerly
|
||||||
|
path: out/kf/*.ttf
|
||||||
|
|
||||||
- name: Zip TTFs for release
|
- name: Zip TTFs for release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: cd out/ttf && zip -j ../../Readerly.zip *.ttf
|
run: |
|
||||||
|
cd out/ttf && zip -j ../../Readerly.zip *.ttf
|
||||||
|
cd ../../out/kf && zip -j ../../KF_Readerly.zip *.ttf
|
||||||
|
|
||||||
- name: Upload release zip
|
- name: Upload release zips
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Readerly-release
|
name: Readerly-release
|
||||||
path: Readerly.zip
|
path: |
|
||||||
|
Readerly.zip
|
||||||
|
KF_Readerly.zip
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: build
|
needs: build
|
||||||
@@ -42,11 +52,22 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Readerly-release
|
name: Readerly-release
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: Readerly.zip
|
draft: false
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
body: |
|
||||||
|
> [!TIP]
|
||||||
|
> **If you are using a Kobo device and reading books purchased from the Kobo Store or reading `kepub` files converted via Calibre**, you should download KF_Readerly.zip, which has fonts slightly altered for optimal kerning for the `kepub` renderer.
|
||||||
|
|
||||||
|
### Learn more
|
||||||
|
|
||||||
|
Readerly is part of the `ebook-fonts` collection. For more information about those fonts, screenshots and how to install them, please consult the [README](https://github.com/nicoverbruggen/ebook-fonts/blob/main/README.md). The FAQ also includes an entry on how to enable ligatures on Kobo devices, which is highly recommended.
|
||||||
|
files: |
|
||||||
|
Readerly.zip
|
||||||
|
KF_Readerly.zip
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ The goal was to get a metrically/visually similar font, without actually copying
|
|||||||
|
|
||||||
To get to the final result, I decided to use the variable font and work on it. The original is located in `src` and is available under the same OFL as the end result, which is included in `LICENSE`.
|
To get to the final result, I decided to use the variable font and work on it. The original is located in `src` and is available under the same OFL as the end result, which is included in `LICENSE`.
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
Two versions are generated via the pipeline of the [latest release](../../releases/latest):
|
||||||
|
|
||||||
|
- **KF_Readerly.zip** — Kobo-optimized TrueType fonts with a legacy kern table and `KF` prefix. Use this if you have a Kobo e-reader, this version contains optimizations made with [Kobo Font Fix](https://github.com/nicoverbruggen/kobo-font-fix).
|
||||||
|
- **Readerly.zip** — The standard, unmodified fonts, as TrueType files. Useful for other e-readers and use on your desktop computer or smartphone.
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
- `src`: Newsreader variable font TTFs
|
- `src`: Newsreader variable font TTFs
|
||||||
|
|||||||
250
build.py
250
build.py
@@ -8,7 +8,7 @@ Orchestrates the full font build pipeline:
|
|||||||
2. Applies vertical scale (scale.py) via FontForge
|
2. Applies vertical scale (scale.py) via FontForge
|
||||||
3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py)
|
3. Applies vertical metrics, line height, rename (metrics.py, lineheight.py, rename.py)
|
||||||
4. Exports to TTF → ./out/ttf/
|
4. Exports to TTF → ./out/ttf/
|
||||||
5. Post-processes TTFs: x-height overshoot clamping, style flags, autohinting
|
5. Post-processes TTFs: style flags, kern pairs, autohinting
|
||||||
|
|
||||||
Uses FontForge (detected automatically).
|
Uses FontForge (detected automatically).
|
||||||
Run with: python3 build.py
|
Run with: python3 build.py
|
||||||
@@ -25,7 +25,7 @@ import textwrap
|
|||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
#
|
#
|
||||||
# Most of these values are safe to tweak. The --customize flag only toggles
|
# Most of these values are safe to tweak. The --customize flag only toggles
|
||||||
# a small subset at runtime (family name, old-style kerning, outline fixes).
|
# a small subset at runtime (family name, outline fixes).
|
||||||
#
|
#
|
||||||
# Quick reference (what each knob does):
|
# Quick reference (what each knob does):
|
||||||
# - REGULAR_VF / ITALIC_VF: input variable fonts from ./src
|
# - REGULAR_VF / ITALIC_VF: input variable fonts from ./src
|
||||||
@@ -36,12 +36,14 @@ import textwrap
|
|||||||
# - LINE_HEIGHT: Typo line height (default line spacing)
|
# - LINE_HEIGHT: Typo line height (default line spacing)
|
||||||
# - SELECTION_HEIGHT: Win/hhea selection box height and clipping
|
# - SELECTION_HEIGHT: Win/hhea selection box height and clipping
|
||||||
# - ASCENDER_RATIO: ascender share of total height
|
# - ASCENDER_RATIO: ascender share of total height
|
||||||
|
# - KERN_PAIRS: explicit GPOS kern pairs (for devices without ligatures)
|
||||||
# - STYLE_MAP: naming/weight metadata per style
|
# - STYLE_MAP: naming/weight metadata per style
|
||||||
|
|
||||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
SRC_DIR = os.path.join(ROOT_DIR, "src")
|
||||||
OUT_DIR = os.path.join(ROOT_DIR, "out")
|
OUT_DIR = os.path.join(ROOT_DIR, "out")
|
||||||
OUT_TTF_DIR = os.path.join(OUT_DIR, "ttf") # generated TTFs
|
OUT_TTF_DIR = os.path.join(OUT_DIR, "ttf") # generated TTFs
|
||||||
|
OUT_KF_DIR = os.path.join(OUT_DIR, "kf") # Kobo (KF) variants
|
||||||
|
|
||||||
REGULAR_VF = os.path.join(SRC_DIR, "Newsreader-VariableFont_opsz,wght.ttf")
|
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")
|
ITALIC_VF = os.path.join(SRC_DIR, "Newsreader-Italic-VariableFont_opsz,wght.ttf")
|
||||||
@@ -94,17 +96,19 @@ ASCENDER_RATIO = 0.8
|
|||||||
# - Other options are left at ttfautohint defaults; uncomment to override.
|
# - Other options are left at ttfautohint defaults; uncomment to override.
|
||||||
AUTOHINT_OPTS = [
|
AUTOHINT_OPTS = [
|
||||||
"--no-info",
|
"--no-info",
|
||||||
"--stem-width-mode=nss",
|
"--stem-width-mode=qss",
|
||||||
# "--hinting-range-min=8",
|
# "--hinting-range-min=8",
|
||||||
# "--hinting-range-max=50",
|
# "--hinting-range-max=50",
|
||||||
# "--hinting-limit=200",
|
# "--hinting-limit=200",
|
||||||
"--increase-x-height=0",
|
"--increase-x-height=14",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Glyphs whose x-height overshoot is an outlier (+12 vs the standard +22).
|
# Explicit kern pairs: (left_glyph, right_glyph, kern_value_in_units).
|
||||||
# The inconsistent overshoot lands between the hinter's snap zones, causing
|
# Negative values tighten spacing. These are added on top of any existing
|
||||||
# these glyphs to render taller than their neighbors on low-res e-ink.
|
# kerning from the source variable font.
|
||||||
CLAMP_XHEIGHT_GLYPHS = ["u", "uogonek"]
|
KERN_PAIRS = [
|
||||||
|
("f", "i", -100), # this emulates the `fi` ligature
|
||||||
|
]
|
||||||
|
|
||||||
# Step 3: Naming and style metadata (used by the rename step)
|
# Step 3: Naming and style metadata (used by the rename step)
|
||||||
STYLE_MAP = {
|
STYLE_MAP = {
|
||||||
@@ -524,19 +528,15 @@ def ff_license_script():
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
def build_export_script(sfd_path, ttf_path, old_kern=True):
|
def build_export_script(sfd_path, ttf_path):
|
||||||
"""Build a FontForge script that opens an .sfd and exports to TTF."""
|
"""Build a FontForge script that opens an .sfd and exports to TTF."""
|
||||||
if old_kern:
|
|
||||||
flags_line = 'flags = ("opentype", "old-kern", "no-FFTM-table", "winkern")'
|
|
||||||
else:
|
|
||||||
flags_line = 'flags = ("opentype", "no-FFTM-table")'
|
|
||||||
return textwrap.dedent(f"""\
|
return textwrap.dedent(f"""\
|
||||||
import fontforge
|
import fontforge
|
||||||
|
|
||||||
f = fontforge.open({sfd_path!r})
|
f = fontforge.open({sfd_path!r})
|
||||||
print("Exporting: " + f.fontname)
|
print("Exporting: " + f.fontname)
|
||||||
|
|
||||||
{flags_line}
|
flags = ("opentype", "no-FFTM-table")
|
||||||
f.generate({ttf_path!r}, flags=flags)
|
f.generate({ttf_path!r}, flags=flags)
|
||||||
|
|
||||||
print(" -> " + {ttf_path!r})
|
print(" -> " + {ttf_path!r})
|
||||||
@@ -606,61 +606,6 @@ def clean_ttf_degenerate_contours(ttf_path):
|
|||||||
font.close()
|
font.close()
|
||||||
|
|
||||||
|
|
||||||
def clamp_xheight_overshoot(ttf_path):
|
|
||||||
"""Clamp outlier x-height overshoots in a TTF in-place.
|
|
||||||
|
|
||||||
Some glyphs (e.g. 'u') have a smaller overshoot than the standard
|
|
||||||
round overshoot, landing between the hinter's snap zones. This
|
|
||||||
flattens them to the true x-height measured from flat-topped glyphs.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
except Exception:
|
|
||||||
print(" [warn] Skipping x-height clamp: fontTools not available", file=sys.stderr)
|
|
||||||
return
|
|
||||||
|
|
||||||
font = TTFont(ttf_path)
|
|
||||||
glyf = font["glyf"]
|
|
||||||
|
|
||||||
# Measure x-height from flat-topped reference glyphs.
|
|
||||||
xheight = 0
|
|
||||||
for ref in ("x", "v"):
|
|
||||||
if ref not in glyf:
|
|
||||||
continue
|
|
||||||
coords = glyf[ref].coordinates
|
|
||||||
if coords:
|
|
||||||
ymax = max(c[1] for c in coords)
|
|
||||||
if ymax > xheight:
|
|
||||||
xheight = ymax
|
|
||||||
|
|
||||||
if xheight == 0:
|
|
||||||
font.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
clamped = []
|
|
||||||
for name in CLAMP_XHEIGHT_GLYPHS:
|
|
||||||
if name not in glyf:
|
|
||||||
continue
|
|
||||||
glyph = glyf[name]
|
|
||||||
coords = glyph.coordinates
|
|
||||||
if not coords:
|
|
||||||
continue
|
|
||||||
ymax = max(c[1] for c in coords)
|
|
||||||
if ymax <= xheight:
|
|
||||||
continue
|
|
||||||
glyph.coordinates = type(coords)(
|
|
||||||
[(x, min(y, xheight)) for x, y in coords]
|
|
||||||
)
|
|
||||||
glyph_set = font.getGlyphSet()
|
|
||||||
if hasattr(glyph, "recalcBounds"):
|
|
||||||
glyph.recalcBounds(glyph_set)
|
|
||||||
clamped.append(name)
|
|
||||||
|
|
||||||
if clamped:
|
|
||||||
font.save(ttf_path)
|
|
||||||
print(f" Clamped x-height overshoot for: {', '.join(clamped)} (xh={xheight})")
|
|
||||||
font.close()
|
|
||||||
|
|
||||||
|
|
||||||
def fix_ttf_style_flags(ttf_path, style_suffix):
|
def fix_ttf_style_flags(ttf_path, style_suffix):
|
||||||
"""Normalize OS/2 fsSelection and head.macStyle for style linking."""
|
"""Normalize OS/2 fsSelection and head.macStyle for style linking."""
|
||||||
@@ -696,6 +641,117 @@ def fix_ttf_style_flags(ttf_path, style_suffix):
|
|||||||
print(f" Normalized style flags for {style_suffix}")
|
print(f" Normalized style flags for {style_suffix}")
|
||||||
|
|
||||||
|
|
||||||
|
def add_kern_pairs(ttf_path):
|
||||||
|
"""Prepend explicit kern pairs to the GPOS kern table.
|
||||||
|
|
||||||
|
Devices that don't support OpenType ligatures (e.g. some e-readers)
|
||||||
|
fall back to individual glyphs. Without kern pairs, combinations
|
||||||
|
like 'fi' render with a visible gap.
|
||||||
|
|
||||||
|
Pairs are inserted at the front of the first PairPos subtable so
|
||||||
|
they take priority even on renderers that truncate the kern list.
|
||||||
|
"""
|
||||||
|
if not KERN_PAIRS:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools.ttLib.tables.otTables import PairValueRecord, ValueRecord
|
||||||
|
except Exception:
|
||||||
|
print(" [warn] Skipping kern pairs: fontTools not available", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
font = TTFont(ttf_path)
|
||||||
|
gpos = font.get("GPOS")
|
||||||
|
if gpos is None:
|
||||||
|
font.close()
|
||||||
|
print(" [warn] No GPOS table, skipping kern pairs", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
# Map glyph names: try cmap first, fall back to glyph order
|
||||||
|
glyph_order = set(font.getGlyphOrder())
|
||||||
|
|
||||||
|
def resolve(name):
|
||||||
|
# If it's a single character, look up via cmap
|
||||||
|
if len(name) == 1:
|
||||||
|
cp = ord(name)
|
||||||
|
if cp in cmap:
|
||||||
|
return cmap[cp]
|
||||||
|
# Otherwise treat as a glyph name
|
||||||
|
if name in glyph_order:
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build resolved pairs
|
||||||
|
pairs = []
|
||||||
|
for left, right, value in KERN_PAIRS:
|
||||||
|
l = resolve(left)
|
||||||
|
r = resolve(right)
|
||||||
|
if l and r:
|
||||||
|
pairs.append((l, r, value))
|
||||||
|
else:
|
||||||
|
print(f" [warn] Kern pair {left}+{right}: glyph not found", file=sys.stderr)
|
||||||
|
|
||||||
|
if not pairs:
|
||||||
|
font.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the first Format 1 (individual pairs) PairPos subtable in kern
|
||||||
|
pair_pos = None
|
||||||
|
for lookup in gpos.table.LookupList.Lookup:
|
||||||
|
if lookup.LookupType == 2: # PairPos
|
||||||
|
for subtable in lookup.SubTable:
|
||||||
|
if subtable.Format == 1:
|
||||||
|
pair_pos = subtable
|
||||||
|
break
|
||||||
|
if pair_pos:
|
||||||
|
break
|
||||||
|
|
||||||
|
if pair_pos is None:
|
||||||
|
font.close()
|
||||||
|
print(" [warn] No Format 1 PairPos subtable found, skipping kern pairs", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for left_glyph, right_glyph, value in pairs:
|
||||||
|
# Find or create the PairSet for the left glyph
|
||||||
|
try:
|
||||||
|
idx = pair_pos.Coverage.glyphs.index(left_glyph)
|
||||||
|
except ValueError:
|
||||||
|
# Left glyph not in coverage — add it
|
||||||
|
pair_pos.Coverage.glyphs.append(left_glyph)
|
||||||
|
from fontTools.ttLib.tables.otTables import PairSet
|
||||||
|
ps = PairSet()
|
||||||
|
ps.PairValueRecord = []
|
||||||
|
ps.PairValueCount = 0
|
||||||
|
pair_pos.PairSet.append(ps)
|
||||||
|
pair_pos.PairSetCount = len(pair_pos.PairSet)
|
||||||
|
idx = len(pair_pos.Coverage.glyphs) - 1
|
||||||
|
|
||||||
|
pair_set = pair_pos.PairSet[idx]
|
||||||
|
|
||||||
|
# Remove existing pair for same right glyph
|
||||||
|
pair_set.PairValueRecord = [
|
||||||
|
pvr for pvr in pair_set.PairValueRecord
|
||||||
|
if pvr.SecondGlyph != right_glyph
|
||||||
|
]
|
||||||
|
|
||||||
|
# Prepend new pair so it appears first
|
||||||
|
pvr = PairValueRecord()
|
||||||
|
pvr.SecondGlyph = right_glyph
|
||||||
|
vr = ValueRecord()
|
||||||
|
vr.XAdvance = value
|
||||||
|
pvr.Value1 = vr
|
||||||
|
pair_set.PairValueRecord.insert(0, pvr)
|
||||||
|
pair_set.PairValueCount = len(pair_set.PairValueRecord)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
font.save(ttf_path)
|
||||||
|
font.close()
|
||||||
|
print(f" Added {count} kern pair(s) to GPOS")
|
||||||
|
|
||||||
|
|
||||||
def autohint_ttf(ttf_path):
|
def autohint_ttf(ttf_path):
|
||||||
"""Run ttfautohint to add proper TrueType hinting.
|
"""Run ttfautohint to add proper TrueType hinting.
|
||||||
|
|
||||||
@@ -752,6 +808,42 @@ def check_ttfautohint():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
KOBOFIX_URL = (
|
||||||
|
"https://raw.githubusercontent.com/nicoverbruggen/kobo-font-fix/main/kobofix.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _download_kobofix(dest):
|
||||||
|
"""Download kobofix.py if not already cached."""
|
||||||
|
import urllib.request
|
||||||
|
print(f" Downloading kobofix.py ...")
|
||||||
|
urllib.request.urlretrieve(KOBOFIX_URL, dest)
|
||||||
|
print(f" Saved to {dest}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_kobofix(kobofix_path, variant_names):
|
||||||
|
"""Run kobofix.py --preset kf on built TTFs, move KF_ files to out/kf/."""
|
||||||
|
ttf_files = [os.path.join(OUT_TTF_DIR, f"{n}.ttf") for n in variant_names]
|
||||||
|
cmd = [sys.executable, kobofix_path, "--preset", "kf"] + ttf_files
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout, end="")
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("\nERROR: kobofix.py failed", file=sys.stderr)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(OUT_KF_DIR, exist_ok=True)
|
||||||
|
import glob
|
||||||
|
moved = 0
|
||||||
|
for kf_file in glob.glob(os.path.join(OUT_TTF_DIR, "KF_*.ttf")):
|
||||||
|
dest = os.path.join(OUT_KF_DIR, os.path.basename(kf_file))
|
||||||
|
shutil.move(kf_file, dest)
|
||||||
|
moved += 1
|
||||||
|
print(f" Moved {moved} KF font(s) to {OUT_KF_DIR}/")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(" Readerly Build")
|
print(" Readerly Build")
|
||||||
@@ -763,20 +855,16 @@ def main():
|
|||||||
print(f" ttfautohint: {shutil.which('ttfautohint')}")
|
print(f" ttfautohint: {shutil.which('ttfautohint')}")
|
||||||
|
|
||||||
family = DEFAULT_FAMILY
|
family = DEFAULT_FAMILY
|
||||||
old_kern = False
|
|
||||||
outline_fix = True
|
outline_fix = True
|
||||||
|
|
||||||
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 [{DEFAULT_FAMILY}]: ").strip() or DEFAULT_FAMILY
|
||||||
old_kern_input = input(" Export with old-style kerning? [y/N]: ").strip().lower()
|
|
||||||
old_kern = old_kern_input in ("y", "yes")
|
|
||||||
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")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f" Family: {family}")
|
print(f" Family: {family}")
|
||||||
print(f" Old kern: {'yes' if old_kern else 'no'}")
|
|
||||||
print(f" Outline fix: {'yes' if outline_fix else 'no'}")
|
print(f" Outline fix: {'yes' if outline_fix else 'no'}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -786,12 +874,12 @@ def main():
|
|||||||
os.makedirs(tmp_dir)
|
os.makedirs(tmp_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_build(tmp_dir, family=family, old_kern=old_kern, outline_fix=outline_fix)
|
_build(tmp_dir, family=family, outline_fix=outline_fix)
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True, outline_fix=True):
|
def _build(tmp_dir, family=DEFAULT_FAMILY, outline_fix=True):
|
||||||
variants = [(f"{family}-{style}", vf, wght, opsz)
|
variants = [(f"{family}-{style}", vf, wght, opsz)
|
||||||
for style, vf, wght, opsz in VARIANT_STYLES]
|
for style, vf, wght, opsz in VARIANT_STYLES]
|
||||||
variant_names = [name for name, _, _, _ in variants]
|
variant_names = [name for name, _, _, _ in variants]
|
||||||
@@ -883,18 +971,26 @@ def _build(tmp_dir, family=DEFAULT_FAMILY, old_kern=True, outline_fix=True):
|
|||||||
style_suffix = name.split("-")[-1] if "-" in name else "Regular"
|
style_suffix = name.split("-")[-1] if "-" in name else "Regular"
|
||||||
|
|
||||||
# Export TTF
|
# Export TTF
|
||||||
script = build_export_script(sfd_path, ttf_path, old_kern=old_kern)
|
script = build_export_script(sfd_path, ttf_path)
|
||||||
run_fontforge_script(script)
|
run_fontforge_script(script)
|
||||||
if outline_fix:
|
if outline_fix:
|
||||||
clean_ttf_degenerate_contours(ttf_path)
|
clean_ttf_degenerate_contours(ttf_path)
|
||||||
clamp_xheight_overshoot(ttf_path)
|
|
||||||
fix_ttf_style_flags(ttf_path, style_suffix)
|
fix_ttf_style_flags(ttf_path, style_suffix)
|
||||||
|
add_kern_pairs(ttf_path)
|
||||||
autohint_ttf(ttf_path)
|
autohint_ttf(ttf_path)
|
||||||
|
|
||||||
|
|
||||||
|
# Step 5: Generate Kobo (KF) variants via kobofix.py
|
||||||
|
print("\n── Step 5: Generate Kobo (KF) variants ──\n")
|
||||||
|
|
||||||
|
kobofix_path = os.path.join(tmp_dir, "kobofix.py")
|
||||||
|
_download_kobofix(kobofix_path)
|
||||||
|
_run_kobofix(kobofix_path, variant_names)
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print(" Build complete!")
|
print(" Build complete!")
|
||||||
print(f" TTF fonts are in: {OUT_TTF_DIR}/")
|
print(f" TTF fonts are in: {OUT_TTF_DIR}/")
|
||||||
|
print(f" KF fonts are in: {OUT_KF_DIR}/")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user