From 73fcd7d922ed9406a312883da1e1ae5d244d60f8 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 24 Feb 2026 09:57:43 +0900 Subject: [PATCH] otf deva complex combi --- OTFbuild/calligra_font_tests.odt | Bin 6844 -> 11075 bytes OTFbuild/opentype_features.py | 209 +++++++++++++++++++++---------- 2 files changed, 143 insertions(+), 66 deletions(-) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index c2b98d7a1f7fc6d0a063cfb614d0c031c2f4d457..267cdc5bfbfe7657dbdcfbd55f04675be0909481 100644 GIT binary patch delta 10032 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 DMBhPB delta 5759 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 Cb;ywb diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index d947f64..4a0425e 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -5,7 +5,7 @@ Features implemented: - kern: GPOS pair positioning from KemingMachine - liga: Standard ligatures (Alphabetic Presentation Forms) - locl: Bulgarian/Serbian Cyrillic variants -- Devanagari GSUB: nukt, akhn, half, vatu, pres, blws, rphf +- Devanagari GSUB: nukt, akhn (+ conjuncts), half, blwf, cjct, blws, rphf, abvs - Tamil GSUB: consonant+vowel ligatures, KSSA, SHRII - Sundanese GSUB: diacritic combinations - mark: GPOS mark-to-base positioning (diacritics anchors) @@ -453,7 +453,7 @@ def _generate_locl(glyphs, has): def _generate_devanagari(glyphs, has, replacewith_subs=None): - """Generate Devanagari GSUB features: ccmp (consonant mapping + vowel decomposition), nukt, akhn, half, vatu, pres, blws, rphf.""" + """Generate Devanagari GSUB features: ccmp (consonant mapping + vowel decomposition), nukt, akhn (+ conjuncts), half, blwf, cjct, blws, rphf, abvs.""" features = [] # --- ccmp: Map Unicode consonants to internal PUA presentation forms --- @@ -525,8 +525,21 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): if nukt_subs: features.append("feature nukt {\n script dev2;\n" + '\n'.join(nukt_subs) + "\n} nukt;") - # --- akhn: akhand ligatures --- - # Must reference PUA forms after ccmp + # --- akhn: akhand ligatures + conjuncts --- + # All conjunct ligatures (C1 + virama + C2 → ligature) go in akhn + # because HarfBuzz applies akhn with F_GLOBAL masking (all glyphs + # in the syllable can trigger the lookup). The half feature uses + # per-glyph masking (only pre-base consonants), which prevents + # 3-glyph conjuncts from matching across the pre-base/base boundary. + # akhn also runs before half, so conjuncts take priority over + # half-forms when both could match. + def _di(u): + """Convert Unicode Devanagari consonant to internal PUA form.""" + try: + return SC.to_deva_internal(u) + except ValueError: + return u # already PUA or non-consonant + akhn_subs = [] ka_int = SC.to_deva_internal(0x0915) ssa_int = SC.to_deva_internal(0x0937) @@ -540,11 +553,81 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): akhn_subs.append( f" sub {glyph_name(ja_int)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(nya_int)} by {glyph_name(SC.DEVANAGARI_LIG_J_NY)};" ) + + _conjuncts = [ + (0x0915, 0x0924, SC.DEVANAGARI_LIG_K_T, "K.T"), + (0x0918, 0x091F, 0xF01BD, "GH.TT"), + (0x0918, 0x0920, 0xF01BE, "GH.TTH"), + (0x0918, 0x0922, 0xF01BF, "GH.DDH"), + (0x0919, 0x0915, 0xF01CE, "NG.K"), + (0x0919, 0x0916, 0xF01CF, "NG.KH"), + (0x0919, 0x0917, 0xF01D2, "NG.G"), + (0x0919, 0x0918, 0xF01D3, "NG.GH"), + (0x0919, 0x0928, 0xF01CD, "NG.N"), + (0x0919, 0x092E, 0xF01D4, "NG.M"), + (0x091B, 0x0935, 0xF01D5, "CH.V"), + (0x091C, 0x092F, SC.DEVANAGARI_LIG_J_Y, "J.Y"), + (0x091F, 0x0915, 0xF01E0, "TT.K"), + (0x091F, 0x091F, 0xF01D6, "TT.TT"), + (0x091F, 0x0920, 0xF01D7, "TT.TTH"), + (0x091F, 0x092A, 0xF01E1, "TT.P"), + (0x091F, 0x0935, 0xF01D8, "TT.V"), + (0x091F, 0x0936, 0xF01E2, "TT.SH"), + (0x091F, 0x0938, 0xF01E3, "TT.S"), + (0x0920, 0x0920, 0xF01D9, "TTH.TTH"), + (0x0920, 0x0935, 0xF01DA, "TTH.V"), + (0x0921, 0x0917, 0xF01D0, "DD.G"), + (0x0921, 0x0921, 0xF01DB, "DD.DD"), + (0x0921, 0x0922, 0xF01DC, "DD.DDH"), + (0x0921, 0x092D, 0xF01D1, "DD.BH"), + (0x0921, 0x0935, 0xF01DD, "DD.V"), + (0x0922, 0x0922, 0xF01DE, "DDH.DDH"), + (0x0922, 0x0935, 0xF01DF, "DDH.V"), + (0x0924, 0x0924, SC.DEVANAGARI_LIG_T_T, "T.T"), + (0x0926, 0x0917, 0xF01B0, "D.G"), + (0x0926, 0x0918, 0xF01B1, "D.GH"), + (0x0926, 0x0926, 0xF01B2, "D.D"), + (0x0926, 0x0927, 0xF01B3, "D.DH"), + (0x0926, 0x0928, 0xF01B4, "D.N"), + (0x0926, 0x092C, 0xF01B5, "D.B"), + (0x0926, 0x092D, 0xF01B6, "D.BH"), + (0x0926, 0x092E, 0xF01B7, "D.M"), + (0x0926, 0x092F, 0xF01B8, "D.Y"), + (0x0926, 0x0935, 0xF01B9, "D.V"), + (0x0928, 0x0924, SC.DEVANAGARI_LIG_N_T, "N.T"), + (0x0928, 0x0928, SC.DEVANAGARI_LIG_N_N, "N.N"), + (0x092A, 0x091F, 0xF01C0, "P.TT"), + (0x092A, 0x0920, 0xF01C1, "P.TTH"), + (0x092A, 0x0922, 0xF01C2, "P.DDH"), + (0x0936, 0x091A, SC.DEVANAGARI_LIG_SH_C, "SH.C"), + (0x0936, 0x0928, SC.DEVANAGARI_LIG_SH_N, "SH.N"), + (0x0936, 0x0935, SC.DEVANAGARI_LIG_SH_V, "SH.V"), + (0x0937, 0x091F, 0xF01C3, "SS.TT"), + (0x0937, 0x0920, 0xF01C4, "SS.TTH"), + (0x0937, 0x0922, 0xF01C5, "SS.DDH"), + (0x0937, 0x092A, SC.DEVANAGARI_LIG_SS_P, "SS.P"), + (0x0938, 0x0935, SC.DEVANAGARI_LIG_S_V, "S.V"), + (0x0939, 0x0923, 0xF01C6, "H.NN"), + (0x0939, 0x0928, 0xF01C7, "H.N"), + (0x0939, 0x092E, 0xF01C8, "H.M"), + (0x0939, 0x092F, 0xF01C9, "H.Y"), + (0x0939, 0x0932, 0xF01CA, "H.L"), + (0x0939, 0x0935, 0xF01CB, "H.V"), + ] + for c1_uni, c2_uni, result, name in _conjuncts: + c1 = _di(c1_uni) + c2 = _di(c2_uni) + if has(c1) and has(SC.DEVANAGARI_VIRAMA) and has(c2) and has(result): + akhn_subs.append( + f" sub {glyph_name(c1)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(c2)} by {glyph_name(result)}; # {name}" + ) if akhn_subs: features.append("feature akhn {\n script dev2;\n" + '\n'.join(akhn_subs) + "\n} akhn;") # --- half: consonant (PUA) + virama -> half form --- - # After ccmp, consonants are in PUA form, so reference PUA here + # After ccmp, consonants are in PUA form, so reference PUA here. + # Conjuncts are NOT here because half uses per-glyph masking (only + # pre-base consonants), preventing 3-glyph matches across the base. half_subs = [] for uni_cp in range(0x0915, 0x093A): internal = SC.to_deva_internal(uni_cp) @@ -593,67 +676,6 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): 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): - """Convert Unicode Devanagari consonant to internal PUA form.""" - try: - return SC.to_deva_internal(u) - except ValueError: - return u # already PUA or non-consonant - - pres_subs = [] - _conjuncts = [ - (0x0915, 0x0924, SC.DEVANAGARI_LIG_K_T, "K.T"), - (0x0924, 0x0924, SC.DEVANAGARI_LIG_T_T, "T.T"), - (0x0928, 0x0924, SC.DEVANAGARI_LIG_N_T, "N.T"), - (0x0928, 0x0928, SC.DEVANAGARI_LIG_N_N, "N.N"), - (0x0926, 0x0917, 0xF01B0, "D.G"), - (0x0926, 0x0918, 0xF01B1, "D.GH"), - (0x0926, 0x0926, 0xF01B2, "D.D"), - (0x0926, 0x0927, 0xF01B3, "D.DH"), - (0x0926, 0x0928, 0xF01B4, "D.N"), - (0x0926, 0x092C, 0xF01B5, "D.B"), - (0x0926, 0x092D, 0xF01B6, "D.BH"), - (0x0926, 0x092E, 0xF01B7, "D.M"), - (0x0926, 0x092F, 0xF01B8, "D.Y"), - (0x0926, 0x0935, 0xF01B9, "D.V"), - (0x0938, 0x0935, SC.DEVANAGARI_LIG_S_V, "S.V"), - (0x0937, 0x092A, SC.DEVANAGARI_LIG_SS_P, "SS.P"), - (0x0936, 0x091A, SC.DEVANAGARI_LIG_SH_C, "SH.C"), - (0x0936, 0x0928, SC.DEVANAGARI_LIG_SH_N, "SH.N"), - (0x0936, 0x0935, SC.DEVANAGARI_LIG_SH_V, "SH.V"), - (0x0918, 0x091F, 0xF01BD, "GH.TT"), - (0x0918, 0x0920, 0xF01BE, "GH.TTH"), - (0x0918, 0x0922, 0xF01BF, "GH.DDH"), - (0x091F, 0x091F, 0xF01D6, "TT.TT"), - (0x091F, 0x0920, 0xF01D7, "TT.TTH"), - (0x0920, 0x0920, 0xF01D9, "TTH.TTH"), - (0x0921, 0x0921, 0xF01DB, "DD.DD"), - (0x0921, 0x0922, 0xF01DC, "DD.DDH"), - (0x0922, 0x0922, 0xF01DE, "DDH.DDH"), - (0x092A, 0x091F, 0xF01C0, "P.TT"), - (0x092A, 0x0920, 0xF01C1, "P.TTH"), - (0x092A, 0x0922, 0xF01C2, "P.DDH"), - (0x0937, 0x091F, 0xF01C3, "SS.TT"), - (0x0937, 0x0920, 0xF01C4, "SS.TTH"), - (0x0937, 0x0922, 0xF01C5, "SS.DDH"), - (0x0939, 0x0923, 0xF01C6, "H.NN"), - (0x0939, 0x0928, 0xF01C7, "H.N"), - (0x0939, 0x092E, 0xF01C8, "H.M"), - (0x0939, 0x092F, 0xF01C9, "H.Y"), - (0x0939, 0x0932, 0xF01CA, "H.L"), - (0x0939, 0x0935, 0xF01CB, "H.V"), - ] - for c1_uni, c2_uni, result, name in _conjuncts: - c1 = _di(c1_uni) - c2 = _di(c2_uni) - if has(c1) and has(SC.DEVANAGARI_VIRAMA) and has(c2) and has(result): - pres_subs.append( - f" sub {glyph_name(c1)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(c2)} by {glyph_name(result)}; # {name}" - ) - if pres_subs: - features.append("feature pres {\n script dev2;\n" + '\n'.join(pres_subs) + "\n} pres;") - # --- blws: RA/RRA/HA (PUA) + U/UU -> special syllables --- blws_subs = [] _blws_rules = [ @@ -682,6 +704,61 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): ) features.append(rphf_code) + # --- abvs: complex reph substitution --- + # The Kotlin engine uses complex reph (U+F010D) when a + # devanagariSuperscript mark precedes reph, or any vowel matra + # (e.g. i-matra) exists in the syllable. + # After dev2 reordering, glyph order is: + # [pre-base matras] + [base] + [below-base] + [above-base] + [reph] + # We use chaining contextual substitution to detect these conditions. + if has(SC.DEVANAGARI_RA_SUPER) and has(SC.DEVANAGARI_RA_SUPER_COMPLEX): + # Trigger class: union of devanagariVowels and devanagariSuperscripts + trigger_cps = ( + list(range(0x0900, 0x0903)) + + list(range(0x093A, 0x093D)) + + list(range(0x093E, 0x094D)) + + [0x094E, 0x094F, 0x0951] + + list(range(0x0953, 0x0956)) + ) + trigger_glyphs = [glyph_name(cp) for cp in trigger_cps if has(cp)] + + # Broad Devanagari class for context gaps between i-matra and reph + deva_any_cps = ( + list(range(0xF0140, 0xF0165)) + # PUA consonants + list(range(0xF0170, 0xF0195)) + # nukta forms + list(range(0xF0230, 0xF0255)) + # half forms + list(range(0xF0320, 0xF0405)) + # RA-appended forms + list(range(0x093A, 0x094D)) + # vowel signs/matras + list(range(0x0900, 0x0903)) + # signs + [0x094E, 0x094F, 0x0951] + + list(range(0x0953, 0x0956)) + + [SC.DEVANAGARI_RA_SUB] + # below-base RA + [r for _, _, r, _ in _conjuncts] # conjunct result glyphs + ) + deva_any_glyphs = [glyph_name(cp) for cp in sorted(set(deva_any_cps)) if has(cp)] + + if trigger_glyphs and deva_any_glyphs: + reph = glyph_name(SC.DEVANAGARI_RA_SUPER) + complex_reph = glyph_name(SC.DEVANAGARI_RA_SUPER_COMPLEX) + + abvs_lines = [] + abvs_lines.append(f"lookup ComplexReph {{") + abvs_lines.append(f" sub {reph} by {complex_reph};") + abvs_lines.append(f"}} ComplexReph;") + abvs_lines.append("") + abvs_lines.append("feature abvs {") + abvs_lines.append(" script dev2;") + abvs_lines.append(f" @complexRephTriggers = [{' '.join(trigger_glyphs)}];") + abvs_lines.append(f" @devaAny = [{' '.join(deva_any_glyphs)}];") + # Rule 1: trigger mark/vowel immediately before reph + abvs_lines.append(f" sub @complexRephTriggers {reph}' lookup ComplexReph;") + # Rules 2-4: i-matra separated from reph by 1-3 intervening glyphs + abvs_lines.append(f" sub {glyph_name(0x093F)} @devaAny {reph}' lookup ComplexReph;") + abvs_lines.append(f" sub {glyph_name(0x093F)} @devaAny @devaAny {reph}' lookup ComplexReph;") + abvs_lines.append(f" sub {glyph_name(0x093F)} @devaAny @devaAny @devaAny {reph}' lookup ComplexReph;") + abvs_lines.append("} abvs;") + features.append('\n'.join(abvs_lines)) + if not features: return "" return '\n\n'.join(features)