From d94bac6186077960157598cc6a2406eca506aa3f Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 24 Feb 2026 06:01:24 +0900 Subject: [PATCH] otf wip4 --- OTFbuild/calligra_font_tests.odt | Bin 0 -> 6844 bytes OTFbuild/font_builder.py | 17 +++- OTFbuild/opentype_features.py | 141 ++++++++++++++++++++++++++----- OTFbuild/sheet_config.py | 1 + 4 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 OTFbuild/calligra_font_tests.odt diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt new file mode 100644 index 0000000000000000000000000000000000000000..c2b98d7a1f7fc6d0a063cfb614d0c031c2f4d457 GIT binary patch literal 6844 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 R0RHtWcr8EP5d8J`{{VR{dMN+^ literal 0 HcmV?d00001 diff --git a/OTFbuild/font_builder.py b/OTFbuild/font_builder.py index 650b751..6cb4301 100644 --- a/OTFbuild/font_builder.py +++ b/OTFbuild/font_builder.py @@ -182,6 +182,19 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): composed += 1 print(f" Composed {composed} fallback bitmaps") + # Step 3c: Identify combining marks for zero advance width + # Glyphs with write_on_top >= 0 are combining marks positioned via + # GPOS mark-to-base. In OpenType they must have zero advance width; + # otherwise the cursor advances past the base and diacritics appear + # shifted to the right. We record them here but keep props.width + # intact so the mark anchor calculation can use the original width. + mark_cps = set() + for cp, g in glyphs.items(): + if g.props.write_on_top >= 0 and g.props.width > 0: + mark_cps.add(cp) + if mark_cps: + print(f"Step 3c: Found {len(mark_cps)} combining marks to zero in hmtx") + # Step 4: Create glyph order and cmap print("Step 4: Building glyph order and cmap...") glyph_order = [".notdef"] @@ -242,7 +255,7 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): if name == ".notdef" or name not in glyph_set: continue - advance = g.props.width * SCALE + advance = 0 if cp in mark_cps else g.props.width * SCALE # Compute alignment offset (lsb shift). # The Kotlin code draws the full cell at an offset position: @@ -289,7 +302,7 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): name = glyph_name(cp) if name == ".notdef" or name not in glyph_set: continue - advance = g.props.width * SCALE + advance = 0 if cp in mark_cps else g.props.width * SCALE metrics[name] = (advance, 0) fb.setupHorizontalMetrics(metrics) diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 5fce060..d1c9bc8 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -299,21 +299,76 @@ def _generate_hangul_gsub(glyphs, has, jamo_data): def _generate_kern(kern_pairs, has): - """Generate kern feature from pair positioning data.""" + """Generate kern feature using class-based pair positioning. + + Left glyphs with identical kerning patterns are grouped into + classes, producing one compact PairPosFormat2 lookup per class. + This avoids the 16-bit PairSetOffset overflow that occurs when + a single PairPosFormat1 subtable exceeds 65 536 bytes (which + happens with ~855 left glyphs × ~855 right glyphs per set). + """ if not kern_pairs: return "" - lines = ["feature kern {"] - count = 0 - for (left_cp, right_cp), value in sorted(kern_pairs.items()): - if has(left_cp) and has(right_cp): - lines.append(f" pos {glyph_name(left_cp)} {glyph_name(right_cp)} {value};") - count += 1 + from collections import defaultdict - if count == 0: + # Filter to valid pairs and group by left glyph + by_left = {} + for (left_cp, right_cp), value in kern_pairs.items(): + if has(left_cp) and has(right_cp): + by_left.setdefault(left_cp, []).append((right_cp, value)) + + if not by_left: return "" - lines.append("} kern;") - return '\n'.join(lines) + + # Group left glyphs by their complete kerning signature + # (the full set of right-glyph + value pairs) + sig_to_lefts = defaultdict(list) + for left_cp, pairs in by_left.items(): + sig = frozenset(pairs) + sig_to_lefts[sig].append(left_cp) + + print(f" [kern] {len(sig_to_lefts)} unique left-glyph classes " + f"from {len(by_left)} left glyphs") + + # Build class definitions (outside feature block) and lookups + class_defs = [] + lookup_defs = [] + lookup_names = [] + + for i, (sig, left_cps) in enumerate( + sorted(sig_to_lefts.items(), key=lambda x: min(x[1])) + ): + left_name = f"@kL{i}" + left_glyphs = ' '.join(glyph_name(cp) for cp in sorted(left_cps)) + class_defs.append(f"{left_name} = [{left_glyphs}];") + + # Group right glyphs by kern value + val_to_rights = defaultdict(list) + for right_cp, value in sig: + val_to_rights[value].append(right_cp) + + lk_name = f"kern_{i}" + lk_lines = [f"lookup {lk_name} {{"] + lk_lines.append(" lookupflag IgnoreMarks;") + + for value, right_cps in sorted(val_to_rights.items()): + right_name = f"@kR{i}v{abs(value)}" + right_glyphs = ' '.join(glyph_name(cp) for cp in sorted(right_cps)) + class_defs.append(f"{right_name} = [{right_glyphs}];") + lk_lines.append(f" pos {left_name} {right_name} {value};") + + lk_lines.append(f"}} {lk_name};") + lookup_defs.append('\n'.join(lk_lines)) + lookup_names.append(lk_name) + + # Feature block references all lookups + feat_lines = ["feature kern {"] + for ln in lookup_names: + feat_lines.append(f" lookup {ln};") + feat_lines.append("} kern;") + + return '\n'.join(class_defs + [""] + lookup_defs + [""] + feat_lines) def _generate_liga(has): @@ -501,18 +556,42 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): if half_subs: features.append("feature half {\n script dev2;\n" + '\n'.join(half_subs) + "\n} half;") - # --- vatu: consonant (PUA) + virama + RA (PUA) -> RA-appended form --- + # --- blwf: virama + RA -> below-base RA (rakaar) --- + # This serves two purposes: + # 1. Tells HarfBuzz that RA can be below-base during base detection + # (HarfBuzz tests would_substitute([virama, RA], blwf) with ORIGINAL + # Unicode glyphs, before ccmp has run) + # 2. Actually substitutes virama + RA -> rakaar mark during shaping + # (after ccmp, RA is in PUA form) ra_int = SC.to_deva_internal(0x0930) - vatu_subs = [] + ra_sub = SC.DEVANAGARI_RA_SUB + blwf_subs = [] + # Unicode form (for base detection before ccmp) + if has(SC.DEVANAGARI_VIRAMA) and has(0x0930) and has(ra_sub): + blwf_subs.append( + f" sub {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(0x0930)} by {glyph_name(ra_sub)};" + ) + # PUA form (for actual substitution after ccmp) + if has(SC.DEVANAGARI_VIRAMA) and has(ra_int) and has(ra_sub): + blwf_subs.append( + f" sub {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(ra_int)} by {glyph_name(ra_sub)};" + ) + if blwf_subs: + features.append("feature blwf {\n script dev2;\n" + '\n'.join(blwf_subs) + "\n} blwf;") + + # --- cjct: consonant (PUA) + below-base RA -> RA-appended form --- + # After blwf converts virama+RA to rakaar mark, cjct combines it + # with the preceding consonant to produce the ra-appended glyph. + cjct_subs = [] for uni_cp in range(0x0915, 0x093A): internal = SC.to_deva_internal(uni_cp) ra_form = internal + 480 - if has(internal) and has(SC.DEVANAGARI_VIRAMA) and has(ra_int) and has(ra_form): - vatu_subs.append( - f" sub {glyph_name(internal)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(ra_int)} by {glyph_name(ra_form)};" + if has(internal) and has(ra_sub) and has(ra_form): + cjct_subs.append( + f" sub {glyph_name(internal)} {glyph_name(ra_sub)} by {glyph_name(ra_form)};" ) - if vatu_subs: - features.append("feature vatu {\n script dev2;\n" + '\n'.join(vatu_subs) + "\n} vatu;") + if cjct_subs: + features.append("feature cjct {\n script dev2;\n" + '\n'.join(cjct_subs) + "\n} cjct;") # --- pres: named conjunct ligatures (using PUA forms) --- def _di(u): @@ -716,23 +795,39 @@ def _generate_mark(glyphs, has): f"markClass {glyph_name(cp)} {class_name};" ) - lines.append("") - lines.append("feature mark {") - + # Define lookups at top level so they can be referenced from + # multiple script registrations in the mark feature. + lookup_names = [] for mark_type, mark_list in sorted(mark_classes.items()): class_name = f"@mark_type{mark_type}" lookup_name = f"mark_type{mark_type}" - lines.append(f" lookup {lookup_name} {{") + lines.append(f"lookup {lookup_name} {{") for cp, g in sorted(bases_with_anchors.items()): anchor = g.props.diacritics_anchors[mark_type] if mark_type < 6 else None if anchor and (anchor.x_used or anchor.y_used): ax = anchor.x * SC.SCALE ay = (SC.ASCENT // SC.SCALE - anchor.y) * SC.SCALE - lines.append(f" pos base {glyph_name(cp)} mark {class_name};") + lines.append(f" pos base {glyph_name(cp)} mark {class_name};") - lines.append(f" }} {lookup_name};") + lines.append(f"}} {lookup_name};") + lines.append("") + lookup_names.append(lookup_name) + # Register mark lookups under DFLT (for Latin, etc.) + lines.append("feature mark {") + for ln in lookup_names: + lines.append(f" lookup {ln};") lines.append("} mark;") + # For Devanagari, HarfBuzz's Indic v2 shaper uses abvm/blwm + # features for mark positioning, not the generic 'mark' feature. + # Register the same lookups under abvm for dev2 script. + lines.append("") + lines.append("feature abvm {") + lines.append(" script dev2;") + for ln in lookup_names: + lines.append(f" lookup {ln};") + lines.append("} abvm;") + return '\n'.join(lines) diff --git a/OTFbuild/sheet_config.py b/OTFbuild/sheet_config.py index b532cc2..31eb5f9 100644 --- a/OTFbuild/sheet_config.py +++ b/OTFbuild/sheet_config.py @@ -421,6 +421,7 @@ DEVANAGARI_HALF_RYA = 0xF0107 DEVANAGARI_OPEN_YA = 0xF0108 DEVANAGARI_OPEN_HALF_YA = 0xF0109 DEVANAGARI_ALT_HALF_SHA = 0xF010F +DEVANAGARI_RA_SUB = 0xF010A # below-base RA (rakaar); transient glyph for blwf/cjct DEVANAGARI_EYELASH_RA = 0xF010B DEVANAGARI_RA_SUPER = 0xF010C DEVANAGARI_RA_SUPER_COMPLEX = 0xF010D