mirror of
https://github.com/nicoverbruggen/kobo-font-fix.git
synced 2025-11-05 09:30:08 +01:00
Compare commits
3 Commits
27c3aaf522
...
1ed3677d8e
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed3677d8e | |||
| da8b3631ea | |||
| 1dd9a6ab79 |
@@ -87,7 +87,7 @@ This applies the KF prefix, applies 20 percent line spacing and adds a Kobo `ker
|
||||
The `--name` parameter is used to change the name of the font family.
|
||||
|
||||
```bash
|
||||
./kobofix.py --prefix KF --name="Fonty" --line-percent 20 *.ttf
|
||||
./kobofix.py --prefix KF --name="Fonty" --line-percent 20 --remove-hints *.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.
|
||||
|
||||
147
kobofix.py
147
kobofix.py
@@ -211,7 +211,12 @@ class FontProcessor:
|
||||
if style_name != "Regular":
|
||||
full_name += f" {style_name}"
|
||||
|
||||
ps_name = f"{self.prefix}_{family_name.replace(' ', '-')}"
|
||||
# If prefix is empty, don't add it to the PS name
|
||||
if self.prefix:
|
||||
ps_name = f"{self.prefix}_{family_name.replace(' ', '-')}"
|
||||
else:
|
||||
ps_name = family_name.replace(' ', '-')
|
||||
|
||||
if style_name != "Regular":
|
||||
ps_name += f"-{style_name.replace(' ', '')}"
|
||||
|
||||
@@ -232,8 +237,14 @@ class FontProcessor:
|
||||
def _pair_value_to_kern(value1, value2) -> int:
|
||||
"""
|
||||
Compute a legacy kerning value from GPOS PairValue records.
|
||||
|
||||
This logic is specific to converting GPOS (OpenType) kerning to
|
||||
the older 'kern' (TrueType) table format.
|
||||
|
||||
Note: Only XAdvance values are used, as they directly map to kern table semantics
|
||||
(adjusting inter-character spacing). XPlacement values shift glyphs without
|
||||
affecting spacing and cannot be represented in the legacy kern table. To avoid
|
||||
potential issues, XPlacement values are now being ignored.
|
||||
"""
|
||||
kern_value = 0
|
||||
if value1 is not None:
|
||||
@@ -241,17 +252,11 @@ class FontProcessor:
|
||||
if value2 is not None:
|
||||
kern_value += getattr(value2, "XAdvance", 0) or 0
|
||||
|
||||
if kern_value == 0:
|
||||
if value1 is not None:
|
||||
kern_value += getattr(value1, "XPlacement", 0) or 0
|
||||
if value2 is not None:
|
||||
kern_value += getattr(value2, "XPlacement", 0) or 0
|
||||
|
||||
return int(kern_value)
|
||||
|
||||
def _extract_format1_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||
"""Extract kerning pairs from PairPos Format 1 (per-glyph PairSets)."""
|
||||
pairs = defaultdict(int)
|
||||
pairs = {}
|
||||
coverage = getattr(subtable, "Coverage", None)
|
||||
pair_sets = getattr(subtable, "PairSet", [])
|
||||
|
||||
@@ -266,12 +271,15 @@ class FontProcessor:
|
||||
right_glyph = record.SecondGlyph
|
||||
kern_value = self._pair_value_to_kern(record.Value1, record.Value2)
|
||||
if kern_value:
|
||||
pairs[(left_glyph, right_glyph)] += kern_value
|
||||
# Only set if not already present (first value wins)
|
||||
key = (left_glyph, right_glyph)
|
||||
if key not in pairs:
|
||||
pairs[key] = kern_value
|
||||
return pairs
|
||||
|
||||
def _extract_format2_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||
"""Extract kerning pairs from PairPos Format 2 (class-based)."""
|
||||
pairs = defaultdict(int)
|
||||
pairs = {}
|
||||
coverage = getattr(subtable, "Coverage", None)
|
||||
class_def1 = getattr(subtable, "ClassDef1", None)
|
||||
class_def2 = getattr(subtable, "ClassDef2", None)
|
||||
@@ -307,17 +315,29 @@ class FontProcessor:
|
||||
|
||||
for left in left_glyphs:
|
||||
for right in right_glyphs:
|
||||
pairs[(left, right)] += kern_value
|
||||
# Only set if not already present (first value wins)
|
||||
key = (left, right)
|
||||
if key not in pairs:
|
||||
pairs[key] = kern_value
|
||||
return pairs
|
||||
|
||||
def extract_kern_pairs(self, font: TTFont) -> Dict[Tuple[str, str], int]:
|
||||
"""
|
||||
Extract all kerning pairs from GPOS PairPos lookups.
|
||||
Extract kerning pairs from the font.
|
||||
Prioritizes existing 'kern' table over GPOS data if present.
|
||||
GPOS (Glyph Positioning) is the modern standard for kerning in OpenType fonts.
|
||||
This function iterates through the GPOS tables to find all kerning pairs
|
||||
before we convert them to the legacy 'kern' table format.
|
||||
"""
|
||||
pairs = defaultdict(int)
|
||||
pairs = {}
|
||||
|
||||
# If a kern table already exists, use it instead of GPOS
|
||||
if "kern" in font:
|
||||
kern_table = font["kern"]
|
||||
for subtable in getattr(kern_table, "kernTables", []):
|
||||
if hasattr(subtable, "kernTable"):
|
||||
pairs.update(subtable.kernTable)
|
||||
return pairs
|
||||
|
||||
# Otherwise, extract from GPOS
|
||||
if "GPOS" in font:
|
||||
gpos = font["GPOS"].table
|
||||
lookup_list = getattr(gpos, "LookupList", None)
|
||||
@@ -330,12 +350,16 @@ class FontProcessor:
|
||||
if fmt == 1:
|
||||
format1_pairs = self._extract_format1_pairs(subtable)
|
||||
for key, value in format1_pairs.items():
|
||||
pairs[key] += value
|
||||
# Only add if not already present (first value wins)
|
||||
if key not in pairs:
|
||||
pairs[key] = value
|
||||
elif fmt == 2:
|
||||
format2_pairs = self._extract_format2_pairs(subtable)
|
||||
for key, value in format2_pairs.items():
|
||||
pairs[key] += value
|
||||
return dict(pairs)
|
||||
# Only add if not already present (first value wins)
|
||||
if key not in pairs:
|
||||
pairs[key] = value
|
||||
return pairs
|
||||
|
||||
@staticmethod
|
||||
def add_legacy_kern(font: TTFont, kern_pairs: Dict[Tuple[str, str], int]) -> int:
|
||||
@@ -380,29 +404,39 @@ class FontProcessor:
|
||||
if "name" not in font:
|
||||
logger.warning(" No 'name' table found; skipping all name changes")
|
||||
return
|
||||
else:
|
||||
|
||||
if self.prefix:
|
||||
logger.info(" Renaming the font to: " + f"{self.prefix} {metadata.full_name}")
|
||||
adjusted_family_name = f"{self.prefix} {metadata.family_name}"
|
||||
adjusted_full_name = f"{self.prefix} {metadata.full_name}"
|
||||
else:
|
||||
logger.info(" Updating font metadata (no prefix)")
|
||||
adjusted_family_name = metadata.family_name
|
||||
adjusted_full_name = metadata.full_name
|
||||
|
||||
# Update Family Name
|
||||
self._set_name_records(font, 1, f"{self.prefix} {metadata.family_name}")
|
||||
self._set_name_records(font, 1, adjusted_family_name)
|
||||
# Update Subfamily
|
||||
self._set_name_records(font, 2, metadata.style_name)
|
||||
# Update Full Name
|
||||
self._set_name_records(font, 4, f"{self.prefix} {metadata.full_name}")
|
||||
self._set_name_records(font, 4, adjusted_full_name)
|
||||
|
||||
# Update Typographic Family
|
||||
self._set_name_records(font, 16, f"{self.prefix} {metadata.family_name}")
|
||||
self._set_name_records(font, 16, adjusted_family_name)
|
||||
# Update Preferred Subfamily
|
||||
self._set_name_records(font, 17, metadata.style_name)
|
||||
# Update Preferred Family
|
||||
self._set_name_records(font, 18, f"{self.prefix} {metadata.family_name}")
|
||||
self._set_name_records(font, 18, adjusted_family_name)
|
||||
|
||||
# Update Unique ID (ID 3)
|
||||
try:
|
||||
current_unique = font["name"].getName(3, 3, 1).toUnicode()
|
||||
parts = current_unique.split("Version")
|
||||
version_info = f"Version{parts[1]}" if len(parts) == 2 else "Version 1.000"
|
||||
new_unique_id = f"{self.prefix} {metadata.family_name.strip()}:{version_info}"
|
||||
if self.prefix:
|
||||
new_unique_id = f"{self.prefix} {metadata.family_name.strip()}:{version_info}"
|
||||
else:
|
||||
new_unique_id = f"{metadata.family_name.strip()}:{version_info}"
|
||||
if current_unique != new_unique_id:
|
||||
self._set_name_records(font, 3, new_unique_id)
|
||||
except Exception as e:
|
||||
@@ -417,9 +451,16 @@ class FontProcessor:
|
||||
cff = font["CFF "].cff
|
||||
cff_topdict = cff.topDictIndex[0]
|
||||
|
||||
if self.prefix:
|
||||
cff_full_name = f"{self.prefix} {metadata.full_name}"
|
||||
cff_family_name = f"{self.prefix} {metadata.family_name.replace(' ', '_')}"
|
||||
else:
|
||||
cff_full_name = metadata.full_name
|
||||
cff_family_name = metadata.family_name.replace(' ', '_')
|
||||
|
||||
name_mapping = {
|
||||
"FullName": f"{self.prefix} {metadata.full_name}",
|
||||
"FamilyName": f"{self.prefix} {metadata.family_name.replace(' ', '_')}"
|
||||
"FullName": cff_full_name,
|
||||
"FamilyName": cff_family_name
|
||||
}
|
||||
|
||||
for key, new_value in name_mapping.items():
|
||||
@@ -566,6 +607,7 @@ class FontProcessor:
|
||||
font_path: str,
|
||||
new_name: Optional[str] = None,
|
||||
remove_prefix: Optional[str] = None,
|
||||
remove_hints: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Process a single font file.
|
||||
@@ -609,12 +651,23 @@ class FontProcessor:
|
||||
self.update_weight_metadata(font, font_path)
|
||||
|
||||
if kern:
|
||||
had_kern = "kern" in font
|
||||
had_gpos = "GPOS" in font
|
||||
|
||||
kern_pairs = self.extract_kern_pairs(font)
|
||||
if kern_pairs:
|
||||
written = self.add_legacy_kern(font, kern_pairs)
|
||||
logger.info(f" Kerning: extracted {len(kern_pairs)} pairs; wrote {written} to legacy 'kern' table.")
|
||||
if had_kern:
|
||||
logger.info(f" Kerning: 'kern' table already existed, preserved {written} pairs.")
|
||||
else:
|
||||
logger.info(f" Kerning: created 'kern' table from GPOS data with {written} pairs.")
|
||||
else:
|
||||
logger.info(" Kerning: no GPOS kerning found.")
|
||||
if had_kern:
|
||||
logger.info(" Kerning: 'kern' table existed but was empty, no pairs written.")
|
||||
elif had_gpos:
|
||||
logger.info(" Kerning: GPOS table found but contained no kern pairs, no 'kern' table created.")
|
||||
else:
|
||||
logger.info(" Kerning: no kerning data found (no GPOS or 'kern' table), no pairs written.")
|
||||
else:
|
||||
logger.info(" Skipping `kern` step.")
|
||||
|
||||
@@ -624,6 +677,34 @@ class FontProcessor:
|
||||
del font["GPOS"]
|
||||
logger.info(" Removed GPOS table from the font.")
|
||||
|
||||
# Remove TrueType hints if requested
|
||||
if remove_hints:
|
||||
hints_removed = False
|
||||
# Remove fpgm (Font Program) table
|
||||
if "fpgm" in font:
|
||||
del font["fpgm"]
|
||||
hints_removed = True
|
||||
# Remove prep (Control Value Program) table
|
||||
if "prep" in font:
|
||||
del font["prep"]
|
||||
hints_removed = True
|
||||
# Remove cvt (Control Value Table)
|
||||
if "cvt " in font:
|
||||
del font["cvt "]
|
||||
hints_removed = True
|
||||
# Remove hints from glyf table using the built-in removeHinting method
|
||||
if "glyf" in font:
|
||||
for glyph_name in font.getGlyphOrder():
|
||||
glyph = font["glyf"][glyph_name]
|
||||
if hasattr(glyph, 'removeHinting'):
|
||||
glyph.removeHinting()
|
||||
hints_removed = True
|
||||
|
||||
if hints_removed:
|
||||
logger.info(" Removed TrueType hints from the font.")
|
||||
else:
|
||||
logger.info(" No TrueType hints found to remove.")
|
||||
|
||||
output_path = self._generate_output_path(font_path, metadata)
|
||||
font.save(output_path)
|
||||
logger.info(f" Saved: {output_path}")
|
||||
@@ -654,7 +735,10 @@ class FontProcessor:
|
||||
|
||||
style_part = f"-{style_suffix}" if style_suffix else ""
|
||||
|
||||
base_name = f"{self.prefix}_{metadata.family_name.replace(' ', '_')}{style_part}"
|
||||
if self.prefix:
|
||||
base_name = f"{self.prefix}_{metadata.family_name.replace(' ', '_')}{style_part}"
|
||||
else:
|
||||
base_name = f"{metadata.family_name.replace(' ', '_')}{style_part}"
|
||||
|
||||
return os.path.join(dirname, f"{base_name}{ext.lower()}")
|
||||
|
||||
@@ -714,7 +798,7 @@ Examples:
|
||||
parser.add_argument("--name", type=str,
|
||||
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,
|
||||
help=f"Prefix to add to font names. Required. (Default: {DEFAULT_PREFIX})")
|
||||
help=f"Prefix to add to font names. Set to empty string to omit prefix. (Default: {DEFAULT_PREFIX})")
|
||||
parser.add_argument("--line-percent", type=int, default=DEFAULT_LINE_PERCENT,
|
||||
help=f"Line spacing adjustment percentage. Set to 0 to make no changes to line spacing. (Default: {DEFAULT_LINE_PERCENT})")
|
||||
parser.add_argument("--skip-kobo-kern", action="store_true",
|
||||
@@ -725,6 +809,8 @@ Examples:
|
||||
help="Enable verbose output.")
|
||||
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\")")
|
||||
parser.add_argument("--remove-hints", action="store_true",
|
||||
help="Remove TrueType hints from the font. This may improve render quality on some devices and reduce file size.")
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -764,6 +850,7 @@ Examples:
|
||||
font_path,
|
||||
args.name,
|
||||
args.remove_prefix,
|
||||
args.remove_hints,
|
||||
):
|
||||
success_count += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user