diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 8b21787..712c3ed 100644 Binary files a/OTFbuild/calligra_font_tests.odt and b/OTFbuild/calligra_font_tests.odt differ 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