From 95fafe51a97dc41e97a0ec77009500c1d33aaa05 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 1 Mar 2026 10:46:39 +0900 Subject: [PATCH] contextual devanagari anusvara positioning --- OTFbuild/calligra_font_tests.odt | Bin 15534 -> 15680 bytes OTFbuild/opentype_features.py | 167 +++++++++++++++++- OTFbuild/sheet_config.py | 1 + src/assets/devanagari_variable.tga | 2 +- .../gdx/TerrarumSansBitmap.kt | 32 +++- work_files/devanagari_variable.psd | 4 +- 6 files changed, 197 insertions(+), 9 deletions(-) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 8b21787c240c6881adfd33d61c2a8b600006bf6e..712c3edbd579a53cf8bf21768ff93113295eaaf9 100644 GIT binary patch delta 14473 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 delta 14322 zcmZvDV{qWXw`DXFOl;e>ZB1<3wtsOZwr$(?#G2T)GckAmZ|m*W+uHl(R(Id7uI>-r zr|-F^&#T50PC*770u|&x)?la;4<`-&&(&+~fdT)#&JHoNW)>OM*37d$)&C7b8&F)g$FRz!_Y6_d)bFX7? zdHLAJb;t3mQ~VS6Ht_0Ae0Pv5m?*b5`MIt67SPSvIco}obwGXHZS2*gD8i$x8|0JNWG>GT>!n|2>BM{+Uj$>kWxff!X2XD zvzrlDJ7M&P;AdDKswXV@f156y!lJvM$_&7k<<}*l$WMj{|ID5}1fT=HprwPg_a1!% zL?SQQU(it8Th=^AAGsU`D2(cO(_8!s;W%KCXAR9HbeG6X-YCnSf_q!nHpGq++a(96 zScN0kMO~( z>*Os@x6iHK+3`cLC>^l9AJIta%#?JssK5+VF*TdPc z7zUNrwaVA5@fVp-7P@h=%B;r$H`KaCSXgDAY`XE0+;SicrxuF76OL#YjzmvE+JzYo zjBMFV$}Laq#l&C7=N@c1_J(5?HYT7vF0hqxqv4)TW}E#KXJa7x`0ot(qn^aM`MBy0 z)wjv=86&Y&@Z(G~L8~8my|@rl;fJ2; z^ZASb!xr~UQk`lYyDI3~%4CvZJqvl29}I7Q5?%mrhpH^xMzq>?4#HqJH<5Vz%Ak@& zS`>DQLK(}SuHSI^ITkwgIg?}vs7wepgaVIpRg{>0BbEg?BtFh%C#KZtv#BV~N=%f+ zUc=7eQ_sRIpgujX{;WD5pX^DWs(0&^cD7PAOcLumH`!r>SK1n-Bx^MDEZXIwpv!!& z89xB=N68>*{OZvjqkDst8J1Of`$b;Cvv`A&D2?JdoVwdXIJ#PH89iNcKN&+gNX1P~ zFtdFWEW1Yjh79R^T+PoJ(o{1*<%F&;XgZyoYZ(Z-Qsr`gRMe{b(tKakj66sPwp55R zp>;G8yA>koiF!-{8>8&9EHB4@3V&^{cqIgGIh^Yq>ODsV-1OCFSRIRDGsaVwEwOrd z{Ukf4)S_Su3daENDU=C z+`)>V_5OGS=qdQBhqDSaRYpv#R(}8P1CO{L=GKXa(eCC=u;uH~Em!-c=l-E2qaz8N z1TO~H)RjJy+$JH(TmB8l)@Bi=V4|d?EwYhKH9n@#vAlQLx!xjRxvFlj9?^z2*Z$fz z`|>_GURk<}HP)7D?I><_6<7qi+MBzQ?bg>eq=iSGx6N8hYMO0}clk^8*0od(Z=BXs z@Sm9)AJBMf*#drZH;gc8FUyDLq62{Rfa_=JH=m3zsyT~-Jn6R&efuRjblOhbhdvU& zZu4V>I!+NYc`5wb}wa-#kzQ7*8%YGZ&uYmys; zH)wiM2Hex)nG z*v^lH;L zcQD3~S&2ZRgFK!dE?0!p4<58(g-v9X7-f)E>f$HJAMIxd6mm{w72;Tcj8-JHrjRoQ z&KGtabQq3*75DCzOc1O?F~$y>E9AJ=P;j4aKMSX!{LHFb@{!{UN;OmFMbh;Nz73lX zjWqu8Vz11@2wGOYEJBGc3Jul1oR`7<+~57(E5Cz_v* zy440gHYoL326~An7f(09$J&OMt*RH=`yD$jQq#Z^RWoYf;c)cE&BTQ#!J7+NT~BPM z6z)qzTiO5QG9PEQ2d}fX>(xx_C5-*U$@v$eoMnB%cH7(8)*AmwW&y3Zk#0iyy;z(P zbI@yL{OP4eGGmW`YO750A|l80uXkMp)OEY&xi%rnUb%0Dy>f6sKLam9ui~>WWbcE1 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 diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index ef6cb7d..cdcbc71 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -93,6 +93,11 @@ def generate_features(glyphs, kern_pairs, font_glyph_set, if mark_code: parts.append(mark_code) + # Anusvara GPOS (must come AFTER mark so lookups are ordered correctly) + anus_gpos = _generate_anusvara_gpos(glyphs, has) + if anus_gpos: + parts.append(anus_gpos) + return '\n\n'.join(parts) @@ -867,8 +872,9 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): # syllable, so it works for both pre-base I-matra and post-base II-matra. matra_lookups, matra_body = _generate_psts_matra_variants(glyphs, has, _conjuncts) ya_lookups, ya_body = _generate_psts_open_ya(glyphs, has) - all_lookups = matra_lookups + ya_lookups - all_body = matra_body + ya_body + anus_lookups, anus_body = _generate_psts_anusvara(glyphs, has, _conjuncts) + all_lookups = matra_lookups + ya_lookups + anus_lookups + all_body = matra_body + ya_body + anus_body if all_body: feat = ["feature psts {", " script dev2;"] feat.extend(all_body) @@ -1125,6 +1131,93 @@ def _generate_psts_matra_variants(glyphs, has, conjuncts): return lines, psts_i_lines + psts_ii_lines +def _generate_psts_anusvara(glyphs, has, conjuncts): + """Generate psts GSUB rules for contextual Anusvara lower variant. + + When Anusvara (U+0902) is preceded by certain vowel signs or reph, + it is substituted with a lower variant (uF016C). + + Substitution triggers: + - uni093E (AA-matra, directly before anusvara) + - uni094E (prishthamatra, reordered before consonant cluster) + - uni0948 (AI-matra), uni094C (AU-matra), uni094F (AW-matra) + - uF010C / uF010D (reph, directly before anusvara) + + Returns (lookup_lines, feature_body_lines). + """ + anusvara = 0x0902 + anusvara_lower = SC.DEVANAGARI_ANUSVARA_LOWER + + if not has(anusvara) or not has(anusvara_lower): + return [], [] + + lookups = [] + lookups.append(f"lookup AnusvaraLower {{") + lookups.append(f" sub {glyph_name(anusvara)} by {glyph_name(anusvara_lower)};") + lookups.append(f"}} AnusvaraLower;") + + body = [] + + # 094E gap rules (longest-context-first). + # After dev2 reordering, 094E sits before the consonant cluster while + # anusvara sits at the end. Need rules with 1-5 intervening glyphs. + if has(0x094E): + gap_cps = set() + for cp in SC.DEVANAGARI_PRESENTATION_CONSONANTS: + if has(cp): gap_cps.add(cp) + for cp in SC.DEVANAGARI_PRESENTATION_CONSONANTS_HALF: + if has(cp): gap_cps.add(cp) + for cp in SC.DEVANAGARI_PRESENTATION_CONSONANTS_WITH_RA: + if has(cp): gap_cps.add(cp) + for cp in SC.DEVANAGARI_PRESENTATION_CONSONANTS_WITH_RA_HALF: + if has(cp): gap_cps.add(cp) + for _, _, result, _ in conjuncts: + if has(result): gap_cps.add(result) + # Open Ya/Half Ya (substituted by earlier psts rules in same lookup) + for cp in [0xF0108, 0xF0109]: + if has(cp): gap_cps.add(cp) + # Reph forms and below-base RA + for cp in [SC.DEVANAGARI_RA_SUPER, SC.DEVANAGARI_RA_SUPER_COMPLEX, + SC.DEVANAGARI_RA_SUB]: + if has(cp): gap_cps.add(cp) + # Signs and marks + for cp in (list(range(0x0900, 0x0903)) + [0x093C] + + # list(range(0x093A, 0x094D)) + + [0x094F, 0x0951] + list(range(0x0953, 0x0956))): + if has(cp): gap_cps.add(cp) + + if gap_cps: + gap_names = ' '.join(glyph_name(cp) for cp in sorted(gap_cps)) + body.append(f" @anusGap = [{gap_names}];") + for n_gaps in range(5, 0, -1): + gaps = ' @anusGap' * n_gaps + body.append( + f" sub {glyph_name(0x094E)}{gaps}" + f" {glyph_name(anusvara)}' lookup AnusvaraLower;" + ) + + # Direct predecessor triggers + for cp in [0x093E, 0x0948, 0x094C, 0x094F]: + if has(cp): + body.append( + f" sub {glyph_name(cp)}" + f" {glyph_name(anusvara)}' lookup AnusvaraLower;" + ) + + # Reph triggers (directly before anusvara) + for reph_cp in [SC.DEVANAGARI_RA_SUPER, SC.DEVANAGARI_RA_SUPER_COMPLEX]: + if has(reph_cp): + body.append( + f" sub {glyph_name(reph_cp)}" + f" {glyph_name(anusvara)}' lookup AnusvaraLower;" + ) + + if not body: + return [], [] + + return lookups, body + + def _generate_psts_open_ya(glyphs, has): """Generate psts rules for open Ya substitution. @@ -1352,9 +1445,16 @@ def _generate_mark(glyphs, has): else None) has_explicit = anchor and (anchor.x_used or anchor.y_used) - # Determine the anchor x for this mark_type - anchor_x = (anchor.x if (has_explicit and anchor.x_used) - else g.props.width // 2) + # Determine the anchor x for this mark_type. + # Subtract nudge_x because in Kotlin the base position + # already includes -nudgeX (posX = -nudgeX + ...), + # so the anchor is relative to the shifted position. + # In OTF, nudge_x is baked into the contour x_offset + # but not the advance, so the base anchor must also + # account for it. + anchor_x = ((anchor.x if (has_explicit and anchor.x_used) + else g.props.width // 2) + - g.props.nudge_x) ay = ((SC.ASCENT // SC.SCALE - anchor.y) * SC.SCALE if (has_explicit and anchor.y_used) else SC.ASCENT) @@ -1414,3 +1514,60 @@ def _generate_mark(glyphs, has): lines.append("} abvm;") return '\n'.join(lines) + + +def _generate_anusvara_gpos(glyphs, has): + """Generate GPOS contextual positioning for anusvara lower variant. + + When uF016C (anusvara lower) follows certain vowels or reph, it is + shifted right: + - +3px (+150 units) after uni094F or uF010D (complex reph) + - +2px (+100 units) after uni0948, uni094C, or uF010C (simple reph) + + This MUST be appended AFTER _generate_mark() output so its abvm lookups + come after mark-to-base lookups in the LookupList. MarkToBase SETS the + mark offset; the subsequent SinglePos ADDS to it. + """ + anusvara_lower = SC.DEVANAGARI_ANUSVARA_LOWER + + if not has(anusvara_lower): + return "" + + # +3px triggers: uni094F, complex reph + shift3_triggers = [cp for cp in [0x094F, SC.DEVANAGARI_RA_SUPER_COMPLEX] + if has(cp)] + # +2px triggers: uni0948, uni094C, simple reph + shift2_triggers = [cp for cp in [0x093A, 0x0948, 0x094C, SC.DEVANAGARI_RA_SUPER] + if has(cp)] + + if not shift3_triggers and not shift2_triggers: + return "" + + lines = [] + + if shift2_triggers: + lines.append(f"lookup AnusvaraShift2 {{") + lines.append(f" pos {glyph_name(anusvara_lower)} <100 0 0 0>;") + lines.append(f"}} AnusvaraShift2;") + + if shift3_triggers: + lines.append(f"lookup AnusvaraShift3 {{") + lines.append(f" pos {glyph_name(anusvara_lower)} <150 0 0 0>;") + lines.append(f"}} AnusvaraShift3;") + + lines.append("") + lines.append("feature abvm {") + lines.append(" script dev2;") + for cp in shift3_triggers: + lines.append( + f" pos {glyph_name(cp)}" + f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift3;" + ) + for cp in shift2_triggers: + lines.append( + f" pos {glyph_name(cp)}" + f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift2;" + ) + lines.append("} abvm;") + + return '\n'.join(lines) diff --git a/OTFbuild/sheet_config.py b/OTFbuild/sheet_config.py index 31eb5f9..1b8778a 100644 --- a/OTFbuild/sheet_config.py +++ b/OTFbuild/sheet_config.py @@ -452,6 +452,7 @@ DEVANAGARI_LIG_J_J_Y = 0xF01AC MARWARI_LIG_DD_DD = 0xF01BA MARWARI_LIG_DD_DDH = 0xF01BB +DEVANAGARI_ANUSVARA_LOWER = 0xF016C MARWARI_LIG_DD_Y = 0xF016E MARWARI_HALFLIG_DD_Y = 0xF016F diff --git a/src/assets/devanagari_variable.tga b/src/assets/devanagari_variable.tga index f768d71..33cd37f 100644 --- a/src/assets/devanagari_variable.tga +++ b/src/assets/devanagari_variable.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c527cf3f9d802ca7d409687455a8435e64af37eecd77f06df4366d5c38af59f +oid sha256:e67487b4cd96af223b21483b4f4a6c10a2db5b8990d5d15f9ead4491d5c6b283 size 1474578 diff --git a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt index 05c13b9..3726c6b 100755 --- a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt @@ -1235,7 +1235,10 @@ class TerrarumSansBitmap( if (!itsProp.diacriticsAnchors[diacriticsType].xUsed) itsProp.width.div(2) else itsProp.diacriticsAnchors[diacriticsType].x if (itsProp.alignWhere == GlyphProps.ALIGN_RIGHT) { - posXbuffer[nonDiacriticCounter] + anchorPoint + (itsProp.width + 1).div(2) + if (thisChar in 0x900..0x902) + posXbuffer[nonDiacriticCounter] + anchorPoint + (itsProp.width - 1).div(2) + else + posXbuffer[nonDiacriticCounter] + anchorPoint + (itsProp.width + 1).div(2) } else { if (thisChar in 0x900..0x902) posXbuffer[nonDiacriticCounter] + anchorPoint - (W_VAR_INIT + 1) / 2 @@ -1246,6 +1249,19 @@ class TerrarumSansBitmap( else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}") } + // Lower Anusvara: shift right when after certain vowels or reph + if (thisChar == DEVANAGARI_ANUSVARA_LOWER) { + val prev = str.getOrElse(charIndex - 1) { -1 } + val hasSimpleReph = prev == DEVANAGARI_RA_SUPER + val hasComplexReph = prev == DEVANAGARI_RA_SUPER_COMPLEX + val hasReph = hasSimpleReph || hasComplexReph + val effectivePrev = if (hasReph) str.getOrElse(charIndex - 2) { -1 } else prev + if (effectivePrev == 0x094F || hasComplexReph) { + posXbuffer[charIndex] += 3 + } else if (effectivePrev in intArrayOf(0x093A, 0x0948, 0x094C) || hasSimpleReph) { + posXbuffer[charIndex] += 2 + } + } // set Y pos according to diacritics position when (thisProp.stackWhere) { @@ -1873,6 +1889,17 @@ class TerrarumSansBitmap( seq4[i] = 0xF012F - ((w+1).coerceIn(4,19) - 4) } + // Contextual Anusvara: use lower variant after certain vowels/reph + else if (c == 0x0902) { + val hasReph = cPrev == DEVANAGARI_RA_SUPER || cPrev == DEVANAGARI_RA_SUPER_COMPLEX + val effectivePrev = if (hasReph) seq4.getOrElse(i - 2) { -1 } else cPrev + // 094E (prishthamatra) is reordered before the consonant cluster, + // so scan backward to find it + val hasPrishthamatra = (1..5).any { j -> seq4.getOrElse(i - j) { -1 } == 0x094E } + if (effectivePrev in intArrayOf(0x093E, 0x0948, 0x094C, 0x094F) || hasPrishthamatra || hasReph) { + seq4[i] = DEVANAGARI_ANUSVARA_LOWER + } + } i++ @@ -2770,10 +2797,13 @@ class TerrarumSansBitmap( private const val MARWARI_LIG_DD_DD = 0xF01BA private const val MARWARI_LIG_DD_DDH = 0xF01BB + // F016D is assigned as MARWARI_HALF_DD, referenced by compiler directives for MARWARI_LIG_DD_Y and MARWARI_HALFLIG_DD_Y private const val MARWARI_LIG_DD_Y = 0xF016E private const val MARWARI_HALFLIG_DD_Y = 0xF016F private const val MARWARI_LIG_DD_R = 0xF010E + private const val DEVANAGARI_ANUSVARA_LOWER = 0xF016C + private const val SUNDANESE_ING = 0xF0500 private const val SUNDANESE_ENG = 0xF0501 private const val SUNDANESE_EUNG = 0xF0502 diff --git a/work_files/devanagari_variable.psd b/work_files/devanagari_variable.psd index bd2d868..a16c5dc 100644 --- a/work_files/devanagari_variable.psd +++ b/work_files/devanagari_variable.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9560b26ee68771bf0bf459a2026620dc21b69f9f235d0cde66226efb37c733da -size 1453677 +oid sha256:7c010d94824b0fa356fb7fb2251f1fc6d392d6a9cb32f1f97166850d5cc676bc +size 1453741