mirror of
https://github.com/nicoverbruggen/kobo-font-fix.git
synced 2026-04-01 15:00:08 +02:00
Refactor CLI flags into modal options and add presets
This is a big commit that fixes a variety of issues and prevents fonts
from growing far too large without any benefits. Previously, converted
fonts would use multiple subtables for the `kern` table, but sadly those
are usually not read by renderers, so we now don't save al kern pairs,
but we prioritize.
The full list of changes can be found below.
---
Replace individual boolean flags with modal --kern and --hint options:
- --hint {skip,additive,overwrite,strip}: controls hinting behavior,
including new ttfautohint support (additive/overwrite modes)
- --kern {add-legacy-kern,legacy-kern-only,skip}: replaces the old
--skip-kobo-kern and --remove-gpos flags
- --preset {nv,kf}: bundled configurations for common workflows
Add upfront dependency checking (`ttfautohint`, `font-line`) so missing
tools are caught before any processing begins.
Fix GPOS Extension lookup (type 9) support: kern pairs were silently
missed in fonts that wrap PairPos subtables in Extension lookups.
Rework legacy kern table writing to respect format 0 size constraints.
The subtable length field is uint16, limiting a single subtable to
10,920 pairs. Since most renderers (including Kobo) only read the first
subtable, we write exactly one and prioritize pairs by Unicode range.
=> Basic Latin > Latin-1 Supplement > Latin Extended > rest
This way, the most commonly encountered kerning pairs are preserved
when truncation is needed (and it usually is, for quality fonts).
This commit is contained in:
84
README.md
84
README.md
@@ -18,12 +18,17 @@ Python 3, FontTools, `font-line`.
|
|||||||
|
|
||||||
You can install them like so:
|
You can install them like so:
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3 install fonttools
|
pip3 install fonttools
|
||||||
pip3 install font-line
|
pip3 install font-line
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to use the `--hint additive` or `--hint overwrite` options, you also need `ttfautohint`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install ttfautohint # macOS
|
||||||
|
```
|
||||||
|
|
||||||
On macOS, if you're using the built-in version of Python (via Xcode), you may need to first add a folder to your `PATH` to make `font-line` available, like:
|
On macOS, if you're using the built-in version of Python (via Xcode), you may need to first add a folder to your `PATH` to make `font-line` available, like:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -69,90 +74,45 @@ You can customize what the script does. For more information, consult:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Given the right arguments, you can:
|
Given the right arguments, you can:
|
||||||
- Skip the `kern` step
|
- Control kerning behavior (`--kern`): add a legacy kern table (default), remove GPOS after extraction, or skip entirely
|
||||||
|
- Control hinting (`--hint`): strip hints, apply ttfautohint to unhinted fonts, apply ttfautohint to all fonts, or skip (default)
|
||||||
- Use a custom name for a font
|
- Use a custom name for a font
|
||||||
- Use a custom name for the prefix
|
- Use a custom name for the prefix
|
||||||
- Remove the `GPOS` table entirely
|
|
||||||
- Adjust the percentage of the `font-line` setting
|
- Adjust the percentage of the `font-line` setting
|
||||||
- Skip running `font-line` altogether
|
- Skip running `font-line` altogether
|
||||||
|
|
||||||
For debugging purposes, you can run the script with the `--verbose` flag.
|
For debugging purposes, you can run the script with the `--verbose` flag.
|
||||||
|
|
||||||
## Examples
|
## Presets
|
||||||
|
|
||||||
### Generating KF fonts
|
The script includes presets for common workflows. If no preset or flags are provided, you will be prompted to choose one.
|
||||||
|
|
||||||
This applies the KF prefix, applies 20 percent line spacing and adds a Kobo `kern` table. Ideal if you have an existing TrueType font and you want it on your Kobo device.
|
### NV preset
|
||||||
|
|
||||||
The `--name` parameter is used to change the name of the font family.
|
Prepares fonts for the [ebook-fonts](https://github.com/nicoverbruggen/ebook-fonts) repository. Applies the NV prefix and 20% line spacing. Does not modify kerning or hinting.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./kobofix.py --prefix KF --name="Fonty" --line-percent 20 --remove-hints *.ttf
|
./kobofix.py --preset nv *.ttf
|
||||||
```
|
```
|
||||||
|
|
||||||
To process fonts from my [ebook-fonts](https://github.com/nicoverbruggen/ebook-fonts) collection which are prefixed with "NV", you can replace the prefix and make adjustments in bulk.
|
You can override individual settings, for example to use relaxed spacing:
|
||||||
|
|
||||||
To process all fonts with the "Kobo Fix" preset, simply run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./kobofix.py --prefix KF --remove-prefix="NV" --line-percent 0 *.ttf
|
./kobofix.py --preset nv --line-percent 50 *.ttf
|
||||||
```
|
```
|
||||||
|
|
||||||
(In this case, we'll set --line-percent to 0 so the line height changes aren't made, because the fonts in the NV Collection should already have those changes applied.)
|
### KF preset
|
||||||
|
|
||||||
The expected output is then:
|
Prepares KF fonts from NV fonts for use on Kobo devices. Applies the KF prefix, replaces the NV prefix, and adds a legacy kern table. No line spacing changes are made (since NV fonts already have those applied).
|
||||||
|
|
||||||
```
|
|
||||||
nico@m1ni kobo-font-fix % ./kobofix.py --prefix KF --remove-prefix NV *.ttf --line-percent 0
|
|
||||||
|
|
||||||
Processing: NV-Elstob-Bold.ttf
|
|
||||||
--remove-prefix enabled: using 'Elstob' as the new family name.
|
|
||||||
Renaming the font to: KF Elstob Bold
|
|
||||||
PANOSE corrected: bWeight 8->8, bLetterForm 2->2
|
|
||||||
Kerning: extracted 342467 pairs; wrote 342467 to legacy 'kern' table.
|
|
||||||
Saved: KF_Elstob-Bold.ttf
|
|
||||||
Skipping line adjustment step.
|
|
||||||
|
|
||||||
Processing: NV-Elstob-BoldItalic.ttf
|
|
||||||
--remove-prefix enabled: using 'Elstob' as the new family name.
|
|
||||||
Renaming the font to: KF Elstob Bold Italic
|
|
||||||
PANOSE corrected: bWeight 8->8, bLetterForm 3->3
|
|
||||||
Kerning: extracted 300746 pairs; wrote 300746 to legacy 'kern' table.
|
|
||||||
Saved: KF_Elstob-BoldItalic.ttf
|
|
||||||
Skipping line adjustment step.
|
|
||||||
|
|
||||||
Processing: NV-Elstob-Italic.ttf
|
|
||||||
--remove-prefix enabled: using 'Elstob' as the new family name.
|
|
||||||
Renaming the font to: KF Elstob Italic
|
|
||||||
PANOSE corrected: bWeight 5->5, bLetterForm 3->3
|
|
||||||
Kerning: extracted 286857 pairs; wrote 286856 to legacy 'kern' table.
|
|
||||||
Saved: KF_Elstob-Italic.ttf
|
|
||||||
Skipping line adjustment step.
|
|
||||||
|
|
||||||
Processing: NV-Elstob-Regular.ttf
|
|
||||||
--remove-prefix enabled: using 'Elstob' as the new family name.
|
|
||||||
Renaming the font to: KF Elstob
|
|
||||||
PANOSE corrected: bWeight 5->5, bLetterForm 2->2
|
|
||||||
Kerning: extracted 313998 pairs; wrote 313998 to legacy 'kern' table.
|
|
||||||
Saved: KF_Elstob-Regular.ttf
|
|
||||||
Skipping line adjustment step.
|
|
||||||
|
|
||||||
==================================================
|
|
||||||
Processed 4/4 fonts successfully.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generating NV fonts
|
|
||||||
|
|
||||||
Tight spacing, with a custom font family name:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./kobofix.py --prefix NV --name="Fonty" --line-percent 20 --skip-kobo-kern *.ttf
|
./kobofix.py --preset kf *.ttf
|
||||||
```
|
```
|
||||||
|
|
||||||
Relaxed spacing, with a custom font family name:
|
### Custom processing
|
||||||
|
|
||||||
|
You can also specify all flags manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./kobofix.py --prefix NV --name="Fonty" --line-percent 50 --skip-kobo-kern *.ttf
|
./kobofix.py --prefix KF --name="Fonty" --line-percent 20 --kern add-legacy-kern *.ttf
|
||||||
```
|
```
|
||||||
|
|
||||||
You can play around with `--line-percent` to see what works for you.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# ttfconv.py
|
|
||||||
|
|
||||||
# This script converts OTF fonts to TTF fonts using the font-tools library.
|
|
||||||
# It processes a list of font files provided as command-line arguments.
|
|
||||||
|
|
||||||
# The font-tools library must be installed: `pip install fonttools`.
|
|
||||||
|
|
||||||
from fontTools.ttLib import TTFont
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def convert_font(input_file_path):
|
|
||||||
"""
|
|
||||||
Converts a font file to a TTF font file.
|
|
||||||
This function currently assumes the input is OTF and the output is TTF.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_file_path (str): The path to the input font file.
|
|
||||||
"""
|
|
||||||
if not os.path.exists(input_file_path):
|
|
||||||
print(f"❌ Error: The file '{input_file_path}' was not found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
output_file_path = os.path.splitext(input_file_path)[0] + ".ttf"
|
|
||||||
font = TTFont(input_file_path)
|
|
||||||
font.save(output_file_path)
|
|
||||||
|
|
||||||
print(f"✅ Converted: {os.path.basename(input_file_path)} -> {os.path.basename(output_file_path)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ An error occurred during conversion of '{os.path.basename(input_file_path)}': {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python font_converter.py <font_file1> <font_file2> ...")
|
|
||||||
print("Example: python font_converter.py MyFont.otf AnotherFont.otf")
|
|
||||||
print("You can also use a wildcard: python font_converter.py *.otf")
|
|
||||||
else:
|
|
||||||
for file_path in sys.argv[1:]:
|
|
||||||
if file_path.lower().endswith(".otf"):
|
|
||||||
convert_font(file_path)
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Skipping '{file_path}': This script only converts OTF to TTF.")
|
|
||||||
print(f"To convert other formats, please provide the correct extension.")
|
|
||||||
|
|
||||||
print("\nProcessing complete.")
|
|
||||||
240
kobofix.py
240
kobofix.py
@@ -30,11 +30,24 @@ from fontTools.ttLib import TTFont, newTable
|
|||||||
from fontTools.ttLib.tables._k_e_r_n import KernTable_format_0
|
from fontTools.ttLib.tables._k_e_r_n import KernTable_format_0
|
||||||
|
|
||||||
# -------------
|
# -------------
|
||||||
# DEFAULTS
|
# PRESETS
|
||||||
# -------------
|
# -------------
|
||||||
#
|
#
|
||||||
DEFAULT_PREFIX = "KF"
|
PRESETS = {
|
||||||
DEFAULT_LINE_PERCENT = 20
|
"nv": {
|
||||||
|
"prefix": "NV",
|
||||||
|
"line_percent": 20,
|
||||||
|
"kern": "skip",
|
||||||
|
"hint": "skip",
|
||||||
|
},
|
||||||
|
"kf": {
|
||||||
|
"prefix": "KF",
|
||||||
|
"line_percent": 0,
|
||||||
|
"kern": "add-legacy-kern",
|
||||||
|
"hint": "skip",
|
||||||
|
"remove_prefix": "NV",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# -------------
|
# -------------
|
||||||
# STYLE MAPPING
|
# STYLE MAPPING
|
||||||
@@ -73,8 +86,8 @@ class FontProcessor:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
prefix: str = DEFAULT_PREFIX,
|
prefix: str,
|
||||||
line_percent: int = DEFAULT_LINE_PERCENT
|
line_percent: int,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the font processor with configurable values.
|
Initialize the font processor with configurable values.
|
||||||
@@ -334,50 +347,118 @@ class FontProcessor:
|
|||||||
lookup_list = getattr(gpos, "LookupList", None)
|
lookup_list = getattr(gpos, "LookupList", None)
|
||||||
if lookup_list and lookup_list.Lookup:
|
if lookup_list and lookup_list.Lookup:
|
||||||
for lookup in lookup_list.Lookup:
|
for lookup in lookup_list.Lookup:
|
||||||
# Only process Pair Adjustment lookups (type 2)
|
lookup_type = getattr(lookup, "LookupType", None)
|
||||||
if getattr(lookup, "LookupType", None) == 2:
|
subtables = getattr(lookup, "SubTable", [])
|
||||||
for subtable in getattr(lookup, "SubTable", []):
|
|
||||||
|
# Unwrap Extension lookups (type 9) to get the inner subtables
|
||||||
|
if lookup_type == 9:
|
||||||
|
unwrapped = []
|
||||||
|
for ext_subtable in subtables:
|
||||||
|
ext_type = getattr(ext_subtable, "ExtensionLookupType", None)
|
||||||
|
inner = getattr(ext_subtable, "ExtSubTable", None)
|
||||||
|
if ext_type == 2 and inner is not None:
|
||||||
|
unwrapped.append(inner)
|
||||||
|
subtables = unwrapped
|
||||||
|
lookup_type = 2 if unwrapped else None
|
||||||
|
|
||||||
|
if lookup_type != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for subtable in subtables:
|
||||||
fmt = getattr(subtable, "Format", None)
|
fmt = getattr(subtable, "Format", None)
|
||||||
if fmt == 1:
|
if fmt == 1:
|
||||||
format1_pairs = self._extract_format1_pairs(subtable)
|
extracted = self._extract_format1_pairs(subtable)
|
||||||
for key, value in format1_pairs.items():
|
|
||||||
# Only add if not already present (first value wins)
|
|
||||||
if key not in pairs:
|
|
||||||
pairs[key] = value
|
|
||||||
elif fmt == 2:
|
elif fmt == 2:
|
||||||
format2_pairs = self._extract_format2_pairs(subtable)
|
extracted = self._extract_format2_pairs(subtable)
|
||||||
for key, value in format2_pairs.items():
|
else:
|
||||||
# Only add if not already present (first value wins)
|
continue
|
||||||
|
for key, value in extracted.items():
|
||||||
if key not in pairs:
|
if key not in pairs:
|
||||||
pairs[key] = value
|
pairs[key] = value
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _glyph_priority(glyph_name: str, cmap_reverse: Dict[str, int]) -> int:
|
||||||
|
"""
|
||||||
|
Assign a priority to a glyph for kern pair sorting.
|
||||||
|
Lower values = higher priority. Pairs involving common glyphs
|
||||||
|
are prioritized so they fit within the subtable size limit.
|
||||||
|
"""
|
||||||
|
cp = cmap_reverse.get(glyph_name)
|
||||||
|
if cp is None:
|
||||||
|
return 4 # unmapped glyphs (ligatures, alternates, etc.)
|
||||||
|
if cp <= 0x007F:
|
||||||
|
return 0 # Basic Latin (A-Z, a-z, digits, punctuation)
|
||||||
|
if cp <= 0x00FF:
|
||||||
|
return 1 # Latin-1 Supplement (accented chars, common symbols)
|
||||||
|
if cp <= 0x024F:
|
||||||
|
return 2 # Latin Extended-A and B
|
||||||
|
return 3 # everything else
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
|
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
|
||||||
"""
|
"""
|
||||||
Create or replace a legacy 'kern' table with the supplied pairs.
|
Create or replace a legacy 'kern' table with the supplied pairs.
|
||||||
Splits into multiple subtables if there are more than 10,000 pairs.
|
|
||||||
|
The legacy kern table format has strict size constraints:
|
||||||
|
- Most renderers (including Kobo's WebKit-based engine) only read the
|
||||||
|
first subtable, so we write exactly one.
|
||||||
|
- Format 0 subtables have a uint16 length field (max 65,535 bytes).
|
||||||
|
With a 14-byte header and 6 bytes per pair, this allows at most
|
||||||
|
(65,535 - 14) / 6 = 10,920 pairs before the length overflows.
|
||||||
|
|
||||||
|
When a font has more pairs than this (common with class-based GPOS
|
||||||
|
kerning, which can expand to 100k+ individual pairs), we prioritize
|
||||||
|
by Unicode range so the most commonly encountered pairs are kept:
|
||||||
|
- Basic Latin (U+0000-007F): English, digits, punctuation
|
||||||
|
- Latin-1 Supplement (U+0080-00FF): Western European accented chars
|
||||||
|
- Latin Extended-A/B (U+0100-024F): Central/Eastern European chars
|
||||||
|
- Everything else and unmapped glyphs (ligatures, alternates)
|
||||||
|
|
||||||
|
This means all English kerning is preserved, most Western European
|
||||||
|
kerning (French, German, Spanish, etc.) is preserved, and only less
|
||||||
|
common extended Latin pairings are dropped when truncation is needed.
|
||||||
"""
|
"""
|
||||||
if not kern_pairs:
|
if not kern_pairs:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
MAX_PAIRS = 10920
|
||||||
|
items = [(tuple(k), int(v)) for k, v in kern_pairs.items() if v]
|
||||||
|
|
||||||
|
if len(items) > MAX_PAIRS:
|
||||||
|
# Build reverse cmap (glyph name -> codepoint) for prioritization
|
||||||
|
cmap_reverse = {}
|
||||||
|
if "cmap" in font:
|
||||||
|
for table in font["cmap"].tables:
|
||||||
|
if hasattr(table, "cmap"):
|
||||||
|
for cp, glyph_name in table.cmap.items():
|
||||||
|
if glyph_name not in cmap_reverse:
|
||||||
|
cmap_reverse[glyph_name] = cp
|
||||||
|
|
||||||
|
# Sort by priority of both glyphs (lower = more common)
|
||||||
|
items.sort(key=lambda pair: (
|
||||||
|
FontProcessor._glyph_priority(pair[0][0], cmap_reverse) +
|
||||||
|
FontProcessor._glyph_priority(pair[0][1], cmap_reverse)
|
||||||
|
))
|
||||||
|
|
||||||
|
logger.warning(f" Kerning: {len(items)} pairs exceed the subtable limit of {MAX_PAIRS}. "
|
||||||
|
f"Keeping the {MAX_PAIRS} most common pairs.")
|
||||||
|
items = items[:MAX_PAIRS]
|
||||||
|
|
||||||
kern_table = newTable("kern")
|
kern_table = newTable("kern")
|
||||||
kern_table.version = 0
|
kern_table.version = 0
|
||||||
kern_table.kernTables = []
|
kern_table.kernTables = []
|
||||||
|
|
||||||
# Max pairs per subtable
|
|
||||||
MAX_PAIRS = 10000
|
|
||||||
items = [(tuple(k), int(v)) for k, v in kern_pairs.items() if v]
|
|
||||||
|
|
||||||
for i in range(0, len(items), MAX_PAIRS):
|
|
||||||
chunk = dict(items[i:i + MAX_PAIRS])
|
|
||||||
subtable = KernTable_format_0()
|
subtable = KernTable_format_0()
|
||||||
subtable.version = 0
|
subtable.version = 0
|
||||||
subtable.length = None
|
subtable.length = None
|
||||||
subtable.coverage = 1
|
subtable.coverage = 1
|
||||||
subtable.kernTable = chunk
|
subtable.kernTable = dict(items)
|
||||||
kern_table.kernTables.append(subtable)
|
kern_table.kernTables.append(subtable)
|
||||||
|
|
||||||
|
# Additional subtables are not created because most renderers
|
||||||
|
# (including Kobo's WebKit-based engine) only read the first one.
|
||||||
|
|
||||||
font["kern"] = kern_table
|
font["kern"] = kern_table
|
||||||
|
|
||||||
return len(items)
|
return len(items)
|
||||||
@@ -645,8 +726,7 @@ class FontProcessor:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def process_font(self,
|
def process_font(self,
|
||||||
kern: bool,
|
kern_mode: str,
|
||||||
remove_gpos: bool,
|
|
||||||
font_path: str,
|
font_path: str,
|
||||||
new_name: Optional[str] = None,
|
new_name: Optional[str] = None,
|
||||||
remove_prefix: Optional[str] = None,
|
remove_prefix: Optional[str] = None,
|
||||||
@@ -693,7 +773,11 @@ class FontProcessor:
|
|||||||
self.check_and_fix_panose(font, font_path)
|
self.check_and_fix_panose(font, font_path)
|
||||||
self.update_weight_metadata(font, font_path)
|
self.update_weight_metadata(font, font_path)
|
||||||
|
|
||||||
if kern:
|
# Note: As of firmware 4.45, Kobo reads GPOS kerning data correctly,
|
||||||
|
# but only when webkitTextRendering=optimizeLegibility is enabled.
|
||||||
|
# Since this setting is disabled by default, a legacy kern table is
|
||||||
|
# still needed for most users.
|
||||||
|
if kern_mode in ("add-legacy-kern", "legacy-kern-only"):
|
||||||
had_kern = "kern" in font
|
had_kern = "kern" in font
|
||||||
had_gpos = "GPOS" in font
|
had_gpos = "GPOS" in font
|
||||||
|
|
||||||
@@ -711,14 +795,12 @@ class FontProcessor:
|
|||||||
logger.info(" Kerning: GPOS table found but contained no kern pairs, no 'kern' table created.")
|
logger.info(" Kerning: GPOS table found but contained no kern pairs, no 'kern' table created.")
|
||||||
else:
|
else:
|
||||||
logger.info(" Kerning: no kerning data found (no GPOS or 'kern' table), no pairs written.")
|
logger.info(" Kerning: no kerning data found (no GPOS or 'kern' table), no pairs written.")
|
||||||
else:
|
|
||||||
logger.info(" Skipping `kern` step.")
|
|
||||||
|
|
||||||
# The GPOS table is removed after the kerning data has been extracted
|
if kern_mode == "legacy-kern-only" and "GPOS" in font:
|
||||||
# and written to the `kern` table. This ensures the information is not lost.
|
|
||||||
if remove_gpos and kern and "GPOS" in font:
|
|
||||||
del font["GPOS"]
|
del font["GPOS"]
|
||||||
logger.info(" Removed GPOS table from the font.")
|
logger.info(" Removed GPOS table from the font.")
|
||||||
|
else:
|
||||||
|
logger.info(" Skipping `kern` step.")
|
||||||
|
|
||||||
if hint_mode == "strip":
|
if hint_mode == "strip":
|
||||||
self.strip_hints(font)
|
self.strip_hints(font)
|
||||||
@@ -809,62 +891,95 @@ def validate_font_files(font_paths: List[str]) -> Tuple[List[str], List[str]]:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
|
preset_names = ", ".join(PRESETS.keys())
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Process fonts for Kobo e-readers: add prefix, kern table, "
|
description="Process fonts for Kobo e-readers: add prefix, kern table, "
|
||||||
"PANOSE validation, and line adjustments.",
|
"PANOSE validation, and line adjustments.",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog=f"""
|
||||||
|
Presets:
|
||||||
|
nv Prepare fonts for the ebook-fonts repository. Applies NV prefix,
|
||||||
|
20%% line spacing. Does not modify kerning or hinting.
|
||||||
|
kf Prepare KF fonts from NV fonts. Applies KF prefix, replaces NV
|
||||||
|
prefix, adds legacy kern table. No line spacing changes.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
For a default experience, which will prefix the font with KF, add `kern` table and adjust line-height:
|
Using a preset:
|
||||||
%(prog)s *.ttf
|
%(prog)s --preset nv *.ttf
|
||||||
|
%(prog)s --preset kf *.ttf
|
||||||
|
|
||||||
If you want to rename the font:
|
Custom processing:
|
||||||
%(prog)s --prefix KF --name="Fonty" --line-percent 20 *.ttf
|
%(prog)s --prefix KF --name="Fonty" --line-percent 20 --kern add-legacy-kern *.ttf
|
||||||
|
|
||||||
If you want to keep the line-height because for a given font the default was fine:
|
If no preset or flags are provided, you will be prompted to choose a preset.
|
||||||
%(prog)s --line-percent 0 *.ttf
|
|
||||||
|
|
||||||
For improved legacy support, you can remove the GPOS table (not recommended):
|
|
||||||
%(prog)s --prefix KF --name="Fonty" --line-percent 20 --remove-gpos *.ttf
|
|
||||||
|
|
||||||
To remove a specific prefix, like "NV", before applying a new one:
|
|
||||||
%(prog)s --prefix KF --remove-prefix="NV" *.ttf
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("fonts", nargs="+",
|
parser.add_argument("fonts", nargs="+",
|
||||||
help="Font files to process (*.ttf). You can use a wildcard (glob).")
|
help="Font files to process (*.ttf). You can use a wildcard (glob).")
|
||||||
|
parser.add_argument("--preset", type=str, choices=PRESETS.keys(),
|
||||||
|
help=f"Use a preset configuration ({preset_names}).")
|
||||||
parser.add_argument("--name", type=str,
|
parser.add_argument("--name", type=str,
|
||||||
help="Optional new family name for all fonts. Other font metadata like copyright info is unaffected.")
|
help="Optional new family name for all fonts. Other font metadata like copyright info is unaffected.")
|
||||||
parser.add_argument("--prefix", type=str, default=DEFAULT_PREFIX,
|
parser.add_argument("--prefix", type=str,
|
||||||
help=f"Prefix to add to font names. Set to empty string to omit prefix. (Default: {DEFAULT_PREFIX})")
|
help="Prefix to add to font names. Set to empty string to omit prefix.")
|
||||||
parser.add_argument("--line-percent", type=int, default=DEFAULT_LINE_PERCENT,
|
parser.add_argument("--line-percent", type=int,
|
||||||
help=f"Line spacing adjustment percentage. Set to 0 to make no changes to line spacing. (Default: {DEFAULT_LINE_PERCENT})")
|
help="Line spacing adjustment percentage. Set to 0 to make no changes to line spacing.")
|
||||||
parser.add_argument("--skip-kobo-kern", action="store_true",
|
parser.add_argument("--kern", type=str,
|
||||||
help="Skip the creation of the legacy 'kern' table from GPOS data.")
|
choices=["add-legacy-kern", "legacy-kern-only", "skip"],
|
||||||
parser.add_argument("--remove-gpos", action="store_true",
|
help="Kerning mode: 'add-legacy-kern' extracts GPOS pairs into a legacy kern table, "
|
||||||
help="Remove the GPOS table after converting kerning to a 'kern' table. Does not work if `--skip-kobo-kern` is set.")
|
"'legacy-kern-only' does the same but removes the GPOS table afterwards, "
|
||||||
|
"'skip' leaves kerning untouched.")
|
||||||
parser.add_argument("--verbose", action="store_true",
|
parser.add_argument("--verbose", action="store_true",
|
||||||
help="Enable verbose output.")
|
help="Enable verbose output.")
|
||||||
parser.add_argument("--remove-prefix", type=str,
|
parser.add_argument("--remove-prefix", type=str,
|
||||||
help="Remove a leading prefix from font names before applying the new prefix. Only works if `--name` is not used. (e.g., --remove-prefix=\"NV\")")
|
help="Remove a leading prefix from font names before applying the new prefix. Only works if `--name` is not used. (e.g., --remove-prefix=\"NV\")")
|
||||||
parser.add_argument("--hint", type=str, default="skip",
|
parser.add_argument("--hint", type=str,
|
||||||
choices=["skip", "additive", "overwrite", "strip"],
|
choices=["skip", "additive", "overwrite", "strip"],
|
||||||
help="Hinting mode: 'skip' does nothing (default), 'additive' runs ttfautohint on fonts lacking hints, "
|
help="Hinting mode: 'skip' does nothing, 'additive' runs ttfautohint on fonts lacking hints, "
|
||||||
"'overwrite' runs ttfautohint on all fonts, 'strip' removes all TrueType hints.")
|
"'overwrite' runs ttfautohint on all fonts, 'strip' removes all TrueType hints.")
|
||||||
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.remove_gpos and args.skip_kobo_kern:
|
if args.verbose:
|
||||||
parser.error("--remove-gpos and --skip-kobo-kern cannot be used together.")
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Determine which flags were explicitly set by the user
|
||||||
|
manual_flags = {k for k in ("prefix", "line_percent", "kern", "hint", "remove_prefix", "name")
|
||||||
|
if getattr(args, k) is not None}
|
||||||
|
|
||||||
|
# If no preset and no manual flags, prompt the user to choose a preset
|
||||||
|
if args.preset is None and not manual_flags:
|
||||||
|
logger.info("No preset or flags specified. Available presets:")
|
||||||
|
for name, values in PRESETS.items():
|
||||||
|
logger.info(f" {name}")
|
||||||
|
choice = input("\nChoose a preset: ").strip().lower()
|
||||||
|
if choice not in PRESETS:
|
||||||
|
logger.error(f"Unknown preset '{choice}'. Available: {preset_names}")
|
||||||
|
sys.exit(1)
|
||||||
|
args.preset = choice
|
||||||
|
|
||||||
|
# Apply preset values as defaults, then let explicit flags override
|
||||||
|
if args.preset:
|
||||||
|
preset = PRESETS[args.preset]
|
||||||
|
for key, value in preset.items():
|
||||||
|
if key not in manual_flags:
|
||||||
|
setattr(args, key, value)
|
||||||
|
|
||||||
|
# Fill in remaining defaults for any unset flags
|
||||||
|
if args.prefix is None:
|
||||||
|
parser.error("--prefix is required when not using a preset.")
|
||||||
|
if args.line_percent is None:
|
||||||
|
parser.error("--line-percent is required when not using a preset.")
|
||||||
|
if args.kern is None:
|
||||||
|
args.kern = "skip"
|
||||||
|
if args.hint is None:
|
||||||
|
args.hint = "skip"
|
||||||
|
|
||||||
if args.name and args.remove_prefix:
|
if args.name and args.remove_prefix:
|
||||||
parser.error("--name and --remove-prefix cannot be used together. Use --name to set the font name directly, or --remove-prefix to strip an existing prefix.")
|
parser.error("--name and --remove-prefix cannot be used together. Use --name to set the font name directly, or --remove-prefix to strip an existing prefix.")
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
check_dependencies(args.hint, args.line_percent)
|
check_dependencies(args.hint, args.line_percent)
|
||||||
|
|
||||||
valid_files, invalid_files = validate_font_files(args.fonts)
|
valid_files, invalid_files = validate_font_files(args.fonts)
|
||||||
@@ -894,8 +1009,7 @@ Examples:
|
|||||||
success_count = 0
|
success_count = 0
|
||||||
for font_path in valid_files:
|
for font_path in valid_files:
|
||||||
if processor.process_font(
|
if processor.process_font(
|
||||||
not args.skip_kobo_kern,
|
args.kern,
|
||||||
args.remove_gpos,
|
|
||||||
font_path,
|
font_path,
|
||||||
args.name,
|
args.name,
|
||||||
args.remove_prefix,
|
args.remove_prefix,
|
||||||
|
|||||||
Reference in New Issue
Block a user