mirror of
https://github.com/nicoverbruggen/kobo-font-fix.git
synced 2026-02-04 11:50:09 +01:00
Adjust how kern pairs are copied
This commit is contained in:
90
kobofix.py
90
kobofix.py
@@ -232,92 +232,107 @@ class FontProcessor:
|
|||||||
def _pair_value_to_kern(value1, value2) -> int:
|
def _pair_value_to_kern(value1, value2) -> int:
|
||||||
"""
|
"""
|
||||||
Compute a legacy kerning value from GPOS PairValue records.
|
Compute a legacy kerning value from GPOS PairValue records.
|
||||||
|
|
||||||
This logic is specific to converting GPOS (OpenType) kerning to
|
This logic is specific to converting GPOS (OpenType) kerning to
|
||||||
the older 'kern' (TrueType) table format.
|
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
|
kern_value = 0
|
||||||
if value1 is not None:
|
if value1 is not None:
|
||||||
kern_value += getattr(value1, "XAdvance", 0) or 0
|
kern_value += getattr(value1, "XAdvance", 0) or 0
|
||||||
if value2 is not None:
|
if value2 is not None:
|
||||||
kern_value += getattr(value2, "XAdvance", 0) or 0
|
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)
|
return int(kern_value)
|
||||||
|
|
||||||
def _extract_format1_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
def _extract_format1_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||||
"""Extract kerning pairs from PairPos Format 1 (per-glyph PairSets)."""
|
"""Extract kerning pairs from PairPos Format 1 (per-glyph PairSets)."""
|
||||||
pairs = defaultdict(int)
|
pairs = {}
|
||||||
coverage = getattr(subtable, "Coverage", None)
|
coverage = getattr(subtable, "Coverage", None)
|
||||||
pair_sets = getattr(subtable, "PairSet", [])
|
pair_sets = getattr(subtable, "PairSet", [])
|
||||||
|
|
||||||
if not coverage or not hasattr(coverage, "glyphs"):
|
if not coverage or not hasattr(coverage, "glyphs"):
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
for idx, left_glyph in enumerate(coverage.glyphs):
|
for idx, left_glyph in enumerate(coverage.glyphs):
|
||||||
if idx >= len(pair_sets):
|
if idx >= len(pair_sets):
|
||||||
break
|
break
|
||||||
|
|
||||||
for record in getattr(pair_sets[idx], "PairValueRecord", []):
|
for record in getattr(pair_sets[idx], "PairValueRecord", []):
|
||||||
right_glyph = record.SecondGlyph
|
right_glyph = record.SecondGlyph
|
||||||
kern_value = self._pair_value_to_kern(record.Value1, record.Value2)
|
kern_value = self._pair_value_to_kern(record.Value1, record.Value2)
|
||||||
if kern_value:
|
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
|
return pairs
|
||||||
|
|
||||||
def _extract_format2_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
def _extract_format2_pairs(self, subtable) -> Dict[Tuple[str, str], int]:
|
||||||
"""Extract kerning pairs from PairPos Format 2 (class-based)."""
|
"""Extract kerning pairs from PairPos Format 2 (class-based)."""
|
||||||
pairs = defaultdict(int)
|
pairs = {}
|
||||||
coverage = getattr(subtable, "Coverage", None)
|
coverage = getattr(subtable, "Coverage", None)
|
||||||
class_def1 = getattr(subtable, "ClassDef1", None)
|
class_def1 = getattr(subtable, "ClassDef1", None)
|
||||||
class_def2 = getattr(subtable, "ClassDef2", None)
|
class_def2 = getattr(subtable, "ClassDef2", None)
|
||||||
class1_records = getattr(subtable, "Class1Record", [])
|
class1_records = getattr(subtable, "Class1Record", [])
|
||||||
|
|
||||||
if not coverage or not hasattr(coverage, "glyphs"):
|
if not coverage or not hasattr(coverage, "glyphs"):
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
class1_map = getattr(class_def1, "classDefs", {}) if class_def1 else {}
|
class1_map = getattr(class_def1, "classDefs", {}) if class_def1 else {}
|
||||||
left_by_class = defaultdict(list)
|
left_by_class = defaultdict(list)
|
||||||
for glyph in coverage.glyphs:
|
for glyph in coverage.glyphs:
|
||||||
class_idx = class1_map.get(glyph, 0)
|
class_idx = class1_map.get(glyph, 0)
|
||||||
left_by_class[class_idx].append(glyph)
|
left_by_class[class_idx].append(glyph)
|
||||||
|
|
||||||
class2_map = getattr(class_def2, "classDefs", {}) if class_def2 else {}
|
class2_map = getattr(class_def2, "classDefs", {}) if class_def2 else {}
|
||||||
right_by_class = defaultdict(list)
|
right_by_class = defaultdict(list)
|
||||||
for glyph, class_idx in class2_map.items():
|
for glyph, class_idx in class2_map.items():
|
||||||
right_by_class[class_idx].append(glyph)
|
right_by_class[class_idx].append(glyph)
|
||||||
|
|
||||||
for class1_idx, class1_record in enumerate(class1_records):
|
for class1_idx, class1_record in enumerate(class1_records):
|
||||||
left_glyphs = left_by_class.get(class1_idx, [])
|
left_glyphs = left_by_class.get(class1_idx, [])
|
||||||
if not left_glyphs:
|
if not left_glyphs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for class2_idx, class2_record in enumerate(class1_record.Class2Record):
|
for class2_idx, class2_record in enumerate(class1_record.Class2Record):
|
||||||
right_glyphs = right_by_class.get(class2_idx, [])
|
right_glyphs = right_by_class.get(class2_idx, [])
|
||||||
if not right_glyphs:
|
if not right_glyphs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kern_value = self._pair_value_to_kern(class2_record.Value1, class2_record.Value2)
|
kern_value = self._pair_value_to_kern(class2_record.Value1, class2_record.Value2)
|
||||||
if not kern_value:
|
if not kern_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for left in left_glyphs:
|
for left in left_glyphs:
|
||||||
for right in right_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
|
return pairs
|
||||||
|
|
||||||
def extract_kern_pairs(self, font: TTFont) -> Dict[Tuple[str, str], int]:
|
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.
|
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:
|
if "GPOS" in font:
|
||||||
gpos = font["GPOS"].table
|
gpos = font["GPOS"].table
|
||||||
lookup_list = getattr(gpos, "LookupList", None)
|
lookup_list = getattr(gpos, "LookupList", None)
|
||||||
@@ -330,12 +345,16 @@ class FontProcessor:
|
|||||||
if fmt == 1:
|
if fmt == 1:
|
||||||
format1_pairs = self._extract_format1_pairs(subtable)
|
format1_pairs = self._extract_format1_pairs(subtable)
|
||||||
for key, value in format1_pairs.items():
|
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:
|
elif fmt == 2:
|
||||||
format2_pairs = self._extract_format2_pairs(subtable)
|
format2_pairs = self._extract_format2_pairs(subtable)
|
||||||
for key, value in format2_pairs.items():
|
for key, value in format2_pairs.items():
|
||||||
pairs[key] += value
|
# Only add if not already present (first value wins)
|
||||||
return dict(pairs)
|
if key not in pairs:
|
||||||
|
pairs[key] = value
|
||||||
|
return pairs
|
||||||
|
|
||||||
@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:
|
||||||
@@ -609,12 +628,23 @@ class FontProcessor:
|
|||||||
self.update_weight_metadata(font, font_path)
|
self.update_weight_metadata(font, font_path)
|
||||||
|
|
||||||
if kern:
|
if kern:
|
||||||
|
had_kern = "kern" in font
|
||||||
|
had_gpos = "GPOS" in font
|
||||||
|
|
||||||
kern_pairs = self.extract_kern_pairs(font)
|
kern_pairs = self.extract_kern_pairs(font)
|
||||||
if kern_pairs:
|
if kern_pairs:
|
||||||
written = self.add_legacy_kern(font, 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:
|
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:
|
else:
|
||||||
logger.info(" Skipping `kern` step.")
|
logger.info(" Skipping `kern` step.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user