From 49b039a775871e212db59e1706423184a728056f Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 10 May 2026 10:16:36 +0200 Subject: [PATCH] Fix SMP ligature filter and Extension-wrapped GPOS kern dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs in fontconvert_sdcard.py: 1. SMP filter for ligatures was post-pack, which only checked the already-masked low half (always ≤ 0xFFFF), didn't check `lc`, and couldn't catch SMP cps that already had bits truncated by the mask. Filter at source instead — inside extract_ligatures_fonttools when building the `filtered` dict — so any seq with an SMP component or any lig_cp > 0xFFFF is dropped before packing. The chained 3+ char path is naturally covered because `intermediate_cp = filtered[prefix].lig_cp` reads from the same already-filtered table. 2. GPOS kern dispatch checked `actual.LookupType` after unwrapping an Extension subtable. Format-specific subtables (PairPosFormat1/2) don't carry `LookupType`, so this fails for any Extension-wrapped PairPos lookup — i.e. how every modern font (Inter, Source Serif, Bitter, …) ships kern. Use the effective type instead: effective_type = getattr(st, "ExtensionLookupType", lookup.LookupType) so non-Extension lookups read the outer type and Extension lookups read the inner ExtensionLookupType, and Type-2 PairPos lookups actually reach _extract_pairpos_subtable in either case. --- lib/EpdFont/scripts/fontconvert_sdcard.py | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py index 266baf9d..e309f786 100644 --- a/lib/EpdFont/scripts/fontconvert_sdcard.py +++ b/lib/EpdFont/scripts/fontconvert_sdcard.py @@ -385,14 +385,19 @@ def extract_kerning_fonttools(font_path, codepoints, ppem): lookup = gpos.LookupList.Lookup[li] for st in lookup.SubTable: actual = st - # Unwrap Extension (lookup type 9) wrappers + # Unwrap Extension (lookup type 9) wrappers. After unwrapping, + # `lookup.LookupType` is still 9 and the unwrapped subtable + # carries `Format` rather than `LookupType`, so the *effective* + # type for the dispatch below comes from `st.ExtensionLookupType`. if lookup.LookupType == 9 and hasattr(st, 'ExtSubTable'): actual = st.ExtSubTable + effective_type = getattr(st, 'ExtensionLookupType', lookup.LookupType) if hasattr(actual, 'Format'): - if actual.LookupType == 2: + if effective_type == 2: _extract_pairpos_subtable(actual, glyph_to_cp, raw_kern) else: - print(f" Debug: skipping unsupported GPOS kern lookupType={actual.LookupType} (Format={actual.Format})", + print(f" Debug: skipping unsupported GPOS kern lookupType=" + f"{effective_type} (outer={lookup.LookupType}, Format={actual.Format})", file=sys.stderr) font.close() @@ -552,7 +557,13 @@ def extract_ligatures_fonttools(font_path, codepoints): codepoints_set = set(codepoints) filtered = {} for seq, lig_cp in raw_ligatures.items(): - if lig_cp not in codepoints_set: + if lig_cp not in codepoints_set or lig_cp > 0xFFFF: + continue + # The on-disk format packs each ligature component as a uint16. Drop + # any seq with an SMP component here so the chained 3+ char path — + # which uses `intermediate_cp = filtered[prefix].lig_cp` — also stays + # 16-bit safe by construction. + if any(cp > 0xFFFF for cp in seq): continue if all(cp in codepoints_set for cp in seq): filtered[seq] = lig_cp @@ -727,9 +738,10 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F print(f" [{style_label}] Kerning classes: {kern_left_class_count} left, {kern_right_class_count} right, " f"{matrix_size + entries_size} bytes", file=sys.stderr) + # SMP codepoints in ligature inputs / outputs are filtered inside + # extract_ligatures_fonttools (see the codepoints_set filter), so every + # entry returned here is already 16-bit safe. ligature_pairs = extract_ligatures_fonttools(fontfile, all_cps) - # SMP codepoints overflow the uint32 packed-pair encoding; drop them. - ligature_pairs = [(pk, lc) for pk, lc in ligature_pairs if (pk >> 16) <= 0xFFFF and (pk & 0xFFFF) <= 0xFFFF] if len(ligature_pairs) > 255: print(f" [{style_label}] WARNING: {len(ligature_pairs)} ligature pairs exceeds uint8_t max (255), truncating", file=sys.stderr)