From fca02f1a3db37a9cd867e13c12968eec70020cc7 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 24 Feb 2026 21:14:20 +0900 Subject: [PATCH] otf more deva --- OTFbuild/calligra_font_tests.odt | Bin 11075 -> 13564 bytes OTFbuild/opentype_features.py | 231 +++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 267cdc5bfbfe7657dbdcfbd55f04675be0909481..5d1f13096c98cacfb784c799d9e48e6806467e28 100644 GIT binary patch delta 12300 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 set of base cps + for cp in sorted(base_cps): + ax = anchor_x(cp) + var_idx = min(max(ax + 2, 6), 21) - 6 + i_base_groups[var_idx].add(cp) + + # --- Group half consonants by width --- + # Half consonants only contribute their width to the variant calc, + # so we can group them into width-classes to avoid O(n^2) rule explosion. + half_by_width = defaultdict(set) # width -> set of half cps + for half_cp in half_cps: + hw = glyph_width(half_cp) + half_by_width[hw].add(half_cp) + + # --- Group (half_width, base) pairs by variant index --- + # For half+base: var_idx = clamp(half_width + anchor_x + 2, 6, 21) - 6 + # Key: (half_width, var_idx) -> set of base cps + i_hw_base = defaultdict(lambda: defaultdict(set)) + for hw, _ in sorted(half_by_width.items()): + for cp in base_cps: + ax = anchor_x(cp) + var_idx = min(max(hw + ax + 2, 6), 21) - 6 + i_hw_base[hw][var_idx].add(cp) + + # --- Group (half_width1, half_width2, base) by variant index --- + # For half+half+base: var_idx = clamp(hw1 + hw2 + anchor_x + 2, 6, 21) - 6 + i_hww_base = defaultdict(lambda: defaultdict(set)) + half_widths = sorted(half_by_width.keys()) + for hw1 in half_widths: + for hw2 in half_widths: + for cp in base_cps: + ax = anchor_x(cp) + var_idx = min(max(hw1 + hw2 + ax + 2, 6), 21) - 6 + i_hww_base[(hw1, hw2)][var_idx].add(cp) + + # Build psts feature rules + # Rules must be ordered longest-context-first (first match wins) + psts_i_lines = [] + + # Case C: half + half + base (4-glyph context) + # Use width-class groups: @halfW{w} for half consonants of width w + hh_class_idx = 0 + for (hw1, hw2), var_groups in sorted(i_hww_base.items()): + for var_idx, bases in sorted(var_groups.items()): + if not has(0xF0110 + var_idx): + continue + base_names = ' '.join(glyph_name(cp) for cp in sorted(bases)) + h1_names = ' '.join(glyph_name(cp) for cp in sorted(half_by_width[hw1])) + h2_names = ' '.join(glyph_name(cp) for cp in sorted(half_by_width[hw2])) + cls_b = f"@iHH{hh_class_idx}" + cls_h1 = f"@iHH1_{hh_class_idx}" + cls_h2 = f"@iHH2_{hh_class_idx}" + psts_i_lines.append(f" {cls_b} = [{base_names}];") + psts_i_lines.append(f" {cls_h1} = [{h1_names}];") + psts_i_lines.append(f" {cls_h2} = [{h2_names}];") + psts_i_lines.append( + f" sub {glyph_name(0x093F)}' lookup IMatraVar{var_idx} " + f"{cls_h1} {cls_h2} {cls_b};" + ) + hh_class_idx += 1 + + # Case B: half + base (3-glyph context) + hb_class_idx = 0 + for hw, var_groups in sorted(i_hw_base.items()): + for var_idx, bases in sorted(var_groups.items()): + if not has(0xF0110 + var_idx): + continue + base_names = ' '.join(glyph_name(cp) for cp in sorted(bases)) + h_names = ' '.join(glyph_name(cp) for cp in sorted(half_by_width[hw])) + cls_b = f"@iHB{hb_class_idx}" + cls_h = f"@iH{hb_class_idx}" + psts_i_lines.append(f" {cls_b} = [{base_names}];") + psts_i_lines.append(f" {cls_h} = [{h_names}];") + psts_i_lines.append( + f" sub {glyph_name(0x093F)}' lookup IMatraVar{var_idx} " + f"{cls_h} {cls_b};" + ) + hb_class_idx += 1 + + # Case A: base only (2-glyph context) + for var_idx, bases in sorted(i_base_groups.items()): + if not has(0xF0110 + var_idx): + continue + base_names = ' '.join(glyph_name(cp) for cp in sorted(bases)) + cls = f"@iB{var_idx}" + psts_i_lines.append(f" {cls} = [{base_names}];") + psts_i_lines.append( + f" sub {glyph_name(0x093F)}' lookup IMatraVar{var_idx} {cls};" + ) + + else: + psts_i_lines = [] + + # ===== II-matra variant lookups and rules ===== + if has(0x0940) and has_ii_variants: + # Create 16 single-substitution lookups + for var in range(16): + target = 0xF0120 + var + if has(target): + lines.append(f"lookup IIMatraVar{var} {{") + lines.append(f" sub {glyph_name(0x0940)} by {glyph_name(target)};") + lines.append(f"}} IIMatraVar{var};") + + # Group base consonants by II-matra variant index + # Formula: var_idx = 15 - (clamp(width - anchor_x + 1, 4, 19) - 4) + # (0xF012F - result gives the codepoint, so var_idx 0 = 0xF012F, + # var_idx 15 = 0xF0120; we reverse so var_idx 0 maps to 0xF0120) + # Actually from the plan: 0xF012F - (clamp(w+1, 4, 19) - 4) + # where w = width - anchor_x + # So the PUA codepoint = 0xF012F - (clamp(w+1, 4, 19) - 4) + # If we define var_idx as offset from 0xF0120: + # pua = 0xF0120 + var_idx + # var_idx = 0xF012F - (clamp(w+1, 4, 19) - 4) - 0xF0120 + # = 15 - (clamp(w+1, 4, 19) - 4) + ii_base_groups = defaultdict(set) + for cp in sorted(base_cps): + w = glyph_width(cp) - anchor_x(cp) + clamped = min(max(w + 1, 4), 19) - 4 + var_idx = 15 - clamped # 0xF012F - clamped → offset from 0xF0120 + ii_base_groups[var_idx].add(cp) + + psts_ii_lines = [] + for var_idx, bases in sorted(ii_base_groups.items()): + target = 0xF0120 + var_idx + if not has(target): + continue + base_names = ' '.join(glyph_name(cp) for cp in sorted(bases)) + cls = f"@iiB{var_idx}" + psts_ii_lines.append(f" {cls} = [{base_names}];") + psts_ii_lines.append( + f" sub {cls} {glyph_name(0x0940)}' lookup IIMatraVar{var_idx};" + ) + else: + psts_ii_lines = [] + + if not psts_i_lines and not psts_ii_lines: + return "" + + # Assemble the feature block + feat = ["feature psts {", " script dev2;"] + feat.extend(psts_i_lines) + feat.extend(psts_ii_lines) + feat.append("} psts;") + + return '\n'.join(lines + [''] + feat) + + def _generate_tamil(glyphs, has): """Generate Tamil GSUB features.""" subs = []