1
0

Remove overlap

This commit is contained in:
2026-03-02 12:10:36 +01:00
parent bc140a1513
commit 3e82a3ccd7
4 changed files with 66 additions and 17 deletions

View File

@@ -1,6 +1,2 @@
Newsreader (c) 2020 The Newsreader Project Authors (http://github.com/productiontype/Newsreader)
Readerly (c) 2026 Nico Verbruggen (https://github.com/nicoverbruggen/readerly)
Readerly is originally based on the OFL version of Newsreader (9pt) with some alterations,
and is also available under the same OFL license; for legal reasons it is considered a
modified version of the original Newsreader with a new name.

View File

@@ -12,13 +12,15 @@ To get to the final result, I decided to use the variable font and work on it. T
## Project structure
- `./src`: Newsreader variable font TTFs (source of truth)
- `./src`: Newsreader variable font TTFs
- `./scripts`: FontForge Python scripts applied during the build
- `scale.py`: scales lowercase glyphs vertically to increase x-height
- `metrics.py`: sets vertical metrics (OS/2 Typo, Win, hhea)
- `lineheight.py`: adjusts OS/2 Typo metrics to control line spacing
- `rename.py`: updates font name metadata from Newsreader to Readerly
- `version.py`: sets the font version from `./VERSION`
- `./build.py`: The build script to generate Readerly
- `LICENSE`: The OFL license
- `COPYRIGHT`: Copyright information, later embedded in font
- `VERSION`: The version number, later embedded in font
After running `./build.py`, you should get:
- `./out/sfd`: FontForge source files (generated)
- `./out/ttf`: final TTF fonts (generated)
@@ -28,15 +30,36 @@ To get to the final result, I decided to use the variable font and work on it. T
python3 build.py
```
This uses `fontTools.instancer` and the Flatpak version of FontForge to:
The build script (`build.py`) uses `fontTools` and FontForge to transform the Newsreader variable fonts into Readerly. Each step is described below.
1. Instance the variable fonts into static TTFs at configured axis values (opsz, wght)
2. Scale lowercase glyphs (configurable in `scripts/scale.py`)
3. Set vertical metrics, adjust line height, and update font names
4. Export to TTF with old-style kerning in `./out`
#### Step 1: Instancing
The Newsreader variable font supports two axes: optical size (`opsz`) and weight (`wght`). Using `fontTools.instancer`, the variable fonts are pinned to specific axis values to produce static TTFs. A small optical size (`opsz=9`) is used as the starting point because it produces tighter, more compact letterforms that resemble Bookerly's proportions.
Variant configuration (in `build.py`):
- Regular: wght=400, opsz=9
- Regular: wght=450, opsz=9
- Bold: wght=550, opsz=9
- Italic: wght=400, opsz=9
- Italic: wght=450, opsz=9
- BoldItalic: wght=550, opsz=9
#### Step 2: Scaling, condensing, and overlap removal
Three transforms are applied in sequence via FontForge:
- **Vertical scaling** (`scale.py`): Lowercase glyphs are scaled up vertically (and slightly horizontally) to increase the x-height, bringing it closer to Bookerly's proportions.
- **Horizontal condensing** (`condense.py`): All glyphs are narrowed slightly to match Bookerly's more compact character widths.
- **Overlap removal** (`overlaps.py`): Overlapping contours are merged into clean, unified outlines and winding direction is corrected. Variable fonts commonly use overlapping paths to aid interpolation between weights. After instancing, these overlaps remain. While desktop renderers handle this fine, e-readers like Kobo apply synthetic font weight scaling that can cause visible artifacts (gaps, blobs, uneven strokes) when contours overlap. Merging the overlaps into single paths prevents these rendering issues.
#### Step 3: Metrics, naming, version, and copyright
Several metadata scripts are applied via FontForge:
- **Vertical metrics** (`metrics.py`): Measures design landmarks (cap height, ascender, x-height, descender) from actual glyph bounding boxes and sets OS/2 Typo metrics to the ink boundaries. Enables `USE_TYPO_METRICS`.
- **Line height** (`lineheight.py`): Overrides Win/hhea metrics to control line spacing and selection box height. Values are expressed as multiples of the font's UPM (units per em) — the coordinate grid that all glyph measurements are defined in (Newsreader uses 2000 UPM). A line height of 1.0x UPM means lines are spaced exactly one em apart, with an 80/20 ascender/descender split. The selection box height (1.32x UPM) controls the highlighted area when selecting text.
- **Renaming** (`rename.py`): Rewrites all SFNT name table entries from Newsreader to Readerly, and sets the correct PS weight string and OS/2 weight class for each variant.
- **Version** (`version.py`): Sets the font version and `head.fontRevision` from `./VERSION`.
- **Copyright** (`license.py`): Sets the copyright notice from `./COPYRIGHT`.
#### Step 4: Export
The final fonts are exported from FontForge as both SFD (FontForge source) and TTF with old-style kern tables for maximum compatibility with e-reader rendering engines.

View File

@@ -186,6 +186,7 @@ def _build(tmp_dir):
scale_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "scale.py"))
condense_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "condense.py"))
overlap_code = load_script_as_function(os.path.join(SCRIPTS_DIR, "overlaps.py"))
for name in variant_names:
ttf_path = os.path.join(tmp_dir, f"{name}.ttf")
@@ -195,6 +196,7 @@ def _build(tmp_dir):
script = build_per_font_script(ttf_path, sfd_path, [
("Scaling Y", scale_code),
("Condensing X", condense_code),
("Removing overlaps", overlap_code),
])
run_fontforge_script(script)

28
scripts/overlaps.py Normal file
View File

@@ -0,0 +1,28 @@
"""
FontForge: Remove overlapping contours
───────────────────────────────────────
Merges overlapping contours into clean outlines for all glyphs.
Also corrects winding direction, which can get flipped after overlap removal.
This fixes rendering issues on devices (e.g. Kobo) that struggle with
overlapping paths, especially when applying synthetic bold/weight scaling.
Run inside FontForge (or via build.py which sets `f` before running this).
"""
import fontforge
f = fontforge.activeFont()
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# APPLY
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f.selection.all()
f.removeOverlap()
f.correctDirection()
count = sum(1 for g in f.glyphs() if g.isWorthOutputting())
print(f" Removed overlaps and corrected direction for {count} glyphs")
print("Done.")