diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 601d9b8..63b37ac 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -505,12 +505,44 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None): f" sub {glyph_name(src_cp)} by {targets};" ) - if ccmp_subs or vowel_decomp_subs: - ccmp_parts = ["feature ccmp {", " script dev2;"] + # --- ccmp: Contextual Anusvara upper variant --- + # Must be in ccmp (before reordering) because I-matra (U+093F) is pre-base + # and gets reordered before the consonant. In Unicode order (before + # reordering), all matras are adjacent to anusvara: C + matra + anusvara. + # Reph is NOT a substitution trigger (only a GPOS positioning trigger). + anusvara_upper = SC.DEVANAGARI_ANUSVARA_UPPER + anusvara_ccmp_subs = [] + if has(0x0902) and has(anusvara_upper): + anusvara_triggers = [ + 0x093A, 0x093B, 0x093F, 0x0940, + 0x0945, 0x0946, 0x0947, 0x0948, + 0x0949, 0x094A, 0x094B, 0x094C, + 0x094F, + ] + for cp in anusvara_triggers: + if has(cp): + anusvara_ccmp_subs.append( + f" sub {glyph_name(cp)}" + f" {glyph_name(0x0902)}' lookup AnusvaraUpper;" + ) + + if ccmp_subs or vowel_decomp_subs or anusvara_ccmp_subs: + ccmp_parts = [] + # AnusvaraUpper lookup defined OUTSIDE the feature block so it only + # fires when referenced by contextual rules (not unconditionally). + if anusvara_ccmp_subs: + ccmp_parts.append(f"lookup AnusvaraUpper {{") + ccmp_parts.append(f" sub {glyph_name(0x0902)} by {glyph_name(anusvara_upper)};") + ccmp_parts.append(f"}} AnusvaraUpper;") + ccmp_parts.append("") + ccmp_parts.append("feature ccmp {") + ccmp_parts.append(" script dev2;") if ccmp_subs: ccmp_parts.append(" lookup DevaConsonantMap {") ccmp_parts.extend(" " + s for s in ccmp_subs) ccmp_parts.append(" } DevaConsonantMap;") + if anusvara_ccmp_subs: + ccmp_parts.extend(anusvara_ccmp_subs) if vowel_decomp_subs: ccmp_parts.append(" lookup DevaVowelDecomp {") ccmp_parts.extend(" " + s for s in vowel_decomp_subs) @@ -1132,51 +1164,11 @@ def _generate_psts_matra_variants(glyphs, has, conjuncts): def _generate_psts_anusvara(glyphs, has, conjuncts): - """Generate psts GSUB rules for contextual Anusvara lower variant. + """No longer used — anusvara GSUB moved to ccmp (pre-reordering). - 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) - - uni0948 (AI-matra), uni094C (AU-matra), uni094F (AW-matra) - - uF010C / uF010D (reph, directly before anusvara) - - Returns (lookup_lines, feature_body_lines). + Returns empty lists for backwards compatibility with the psts assembly. """ - 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 = [] - - # Direct predecessor triggers - for cp in [0x093A, 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 + return [], [] def _generate_psts_open_ya(glyphs, has): @@ -1479,70 +1471,118 @@ def _generate_mark(glyphs, has): def _generate_anusvara_gpos(glyphs, has): - """Generate GPOS contextual positioning for anusvara lower variant. + """Generate GPOS contextual positioning for both anusvara forms. - 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) + For uF016C (anusvara upper): + - complex reph: +3px X, -2px Y + - uni094F (or reph + 094F): +3px X + - 0x093A, 0x0948, 0x094C (or reph + these): +2px X + + For uni0902 (regular anusvara): + - complex reph: +3px X, -2px Y + - simple reph: +2px X 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 + anusvara_upper = SC.DEVANAGARI_ANUSVARA_UPPER + anusvara = 0x0902 + complex_reph = SC.DEVANAGARI_RA_SUPER_COMPLEX + simple_reph = SC.DEVANAGARI_RA_SUPER - if not has(anusvara_lower): - return "" + has_upper = has(anusvara_upper) + has_regular = has(anusvara) - # +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 [0x0948, 0x094C, SC.DEVANAGARI_RA_SUPER] - if has(cp)] - - shift2_up2_triggers = [cp for cp in [0x093A] - if has(cp)] - - if not shift3_triggers and not shift2_triggers and not shift2_up2_triggers: + if not has_upper and not has_regular: 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;") + # --- Lookups for anusvara upper (uF016C) --- + if has_upper: + lines.append(f"lookup AnusvaraUpperShift2 {{") + lines.append(f" pos {glyph_name(anusvara_upper)} <100 0 0 0>;") + lines.append(f"}} AnusvaraUpperShift2;") - if shift2_up2_triggers: - lines.append(f"lookup AnusvaraShift2Up2 {{") - lines.append(f" pos {glyph_name(anusvara_lower)} <100 100 0 0>;") # float up by two pixels. This is a hack - lines.append(f"}} AnusvaraShift2Up2;") + lines.append(f"lookup AnusvaraUpperShift3 {{") + lines.append(f" pos {glyph_name(anusvara_upper)} <150 0 0 0>;") + lines.append(f"}} AnusvaraUpperShift3;") - 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(f"lookup AnusvaraUpperShift3Down2 {{") + lines.append(f" pos {glyph_name(anusvara_upper)} <150 -100 0 0>;") + lines.append(f"}} AnusvaraUpperShift3Down2;") + + # --- Lookups for regular anusvara (uni0902) --- + if has_regular: + lines.append(f"lookup AnusvaraRegShift2 {{") + lines.append(f" pos {glyph_name(anusvara)} <100 0 0 0>;") + lines.append(f"}} AnusvaraRegShift2;") + + lines.append(f"lookup AnusvaraRegShift3Down2 {{") + lines.append(f" pos {glyph_name(anusvara)} <150 -100 0 0>;") + lines.append(f"}} AnusvaraRegShift3Down2;") 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;" - ) - for cp in shift2_up2_triggers: - lines.append( - f" pos {glyph_name(cp)}" - f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift2Up2;" - ) + + # --- Rules for anusvara upper (uF016C) --- + # After reordering: base, [matras], reph?, anusvara. + # When reph is present between matra and anusvara, use 3-glyph backtrack. + # Rules ordered longest-context-first (first match wins). + if has_upper: + # Complex reph → always shift3down2 (directly before anusvara) + if has(complex_reph): + lines.append( + f" pos {glyph_name(complex_reph)}" + f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3Down2;" + ) + + # Matra + simple reph + anusvara (3-glyph context: matra in backtrack) + if has(simple_reph): + if has(0x094F): + lines.append( + f" pos {glyph_name(0x094F)} {glyph_name(simple_reph)}" + f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3;" + ) + for cp in [0x093A, 0x0948, 0x094C]: + if has(cp): + lines.append( + f" pos {glyph_name(cp)} {glyph_name(simple_reph)}" + f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift2;" + ) + + # Matra directly before anusvara (no reph) + if has(0x094F): + lines.append( + f" pos {glyph_name(0x094F)}" + f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3;" + ) + for cp in [0x093A, 0x0948, 0x094C]: + if has(cp): + lines.append( + f" pos {glyph_name(cp)}" + f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift2;" + ) + + # --- Rules for regular anusvara (uni0902) --- + # Regular anusvara has no matra trigger (else it would be upper). + # Only reph can trigger a shift here. + if has_regular: + # Complex reph → +3px X, -2px Y + if has(complex_reph): + lines.append( + f" pos {glyph_name(complex_reph)}" + f" {glyph_name(anusvara)}' lookup AnusvaraRegShift3Down2;" + ) + # Simple reph → +2px X + if has(simple_reph): + lines.append( + f" pos {glyph_name(simple_reph)}" + f" {glyph_name(anusvara)}' lookup AnusvaraRegShift2;" + ) + lines.append("} abvm;") return '\n'.join(lines) diff --git a/OTFbuild/sheet_config.py b/OTFbuild/sheet_config.py index 1b8778a..bf4b3b1 100644 --- a/OTFbuild/sheet_config.py +++ b/OTFbuild/sheet_config.py @@ -452,7 +452,7 @@ DEVANAGARI_LIG_J_J_Y = 0xF01AC MARWARI_LIG_DD_DD = 0xF01BA MARWARI_LIG_DD_DDH = 0xF01BB -DEVANAGARI_ANUSVARA_LOWER = 0xF016C +DEVANAGARI_ANUSVARA_UPPER = 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 33cd37f..01c46b4 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:e67487b4cd96af223b21483b4f4a6c10a2db5b8990d5d15f9ead4491d5c6b283 +oid sha256:bab806c7e407feb3de23d28360d5a71c1bbf2ed4f524aacf9a4dcab939bcb712 size 1474578 diff --git a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt index cc7f782..c4060ab 100755 --- a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt @@ -29,7 +29,6 @@ import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.* -import com.badlogic.gdx.utils.GdxRuntimeException import net.torvald.terrarumsansbitmap.DiacriticsAnchor import net.torvald.terrarumsansbitmap.GlyphProps import net.torvald.terrarumsansbitmap.MovableType @@ -42,12 +41,8 @@ import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.OBJ import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.SHY import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ZWSP import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.glueCharToGlueSize -import java.io.BufferedOutputStream -import java.io.FileOutputStream import java.util.* import java.util.zip.CRC32 -import java.util.zip.GZIPInputStream -import kotlin.math.floor import kotlin.math.roundToInt import kotlin.math.sign @@ -1249,20 +1244,31 @@ class TerrarumSansBitmap( else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}") } - // Lower Anusvara: shift right when after certain vowels or reph - if (thisChar == DEVANAGARI_ANUSVARA_LOWER) { + // Upper Anusvara: shift right when after certain vowels or reph + if (thisChar == DEVANAGARI_ANUSVARA_UPPER) { 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) { + if (hasComplexReph) { // Reph can appear regardless of Anusvara state, requiring dupe codes posXbuffer[charIndex] += 3 - } else if (effectivePrev in intArrayOf(0x0948, 0x094C) || hasSimpleReph) { + posYbuffer[charIndex] -= 2 + } else if (effectivePrev == 0x094F) { + posXbuffer[charIndex] += 3 + } else if (effectivePrev in intArrayOf(0x093A, 0x0948, 0x094C)) { posXbuffer[charIndex] += 2 - } else if (effectivePrev == 0x093A) { + } + } + else if (thisChar == 0x0902) { + val prev = str.getOrElse(charIndex - 1) { -1 } + val hasSimpleReph = prev == DEVANAGARI_RA_SUPER + val hasComplexReph = prev == DEVANAGARI_RA_SUPER_COMPLEX + if (hasComplexReph) { // Reph can appear regardless of Anusvara state, requiring dupe codes + posXbuffer[charIndex] += 3 + posYbuffer[charIndex] -= 2 + } else if (hasSimpleReph) { posXbuffer[charIndex] += 2 - posYbuffer[charIndex] += 2 // float up by two pixels. This is a hack } } @@ -1609,6 +1615,17 @@ class TerrarumSansBitmap( seq.add(DEVANAGARI_EYELASH_RA) i += 1 } + // Contextual Anusvara: use upper variant after certain vowels/reph + else if (c == 0x0902) { + if (cPrev in intArrayOf(0x093A, 0x093B, 0x093F, 0x0940, 0x0945, 0x0946, 0x0947, + 0x0948, 0x0949, 0x094A, 0x094B, 0x094C, 0x094F) || + cPrev in 0xF0110..0xF012F) { + + seq.add(DEVANAGARI_ANUSVARA_UPPER) + } + else + seq.add(0x0902) + } // END of devanagari string replacer // rearrange {letter, before-and-after diacritics} as {before-diacritics, letter, after-diacritics} else if (glyphProps[c]?.stackWhere == GlyphProps.STACK_BEFORE_N_AFTER) { @@ -1892,15 +1909,6 @@ 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 - if (effectivePrev in intArrayOf(0x093A, 0x0948, 0x094C, 0x094F) || hasReph) { - seq4[i] = DEVANAGARI_ANUSVARA_LOWER - } - } - i++ } @@ -2802,7 +2810,7 @@ class TerrarumSansBitmap( 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 DEVANAGARI_ANUSVARA_UPPER = 0xF016C private const val SUNDANESE_ING = 0xF0500 private const val SUNDANESE_ENG = 0xF0501 diff --git a/work_files/devanagari_variable.psd b/work_files/devanagari_variable.psd index a16c5dc..66b7a6c 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:7c010d94824b0fa356fb7fb2251f1fc6d392d6a9cb32f1f97166850d5cc676bc -size 1453741 +oid sha256:6171a566806ad2b2a803b6d479946a720eee4a2149df4ffb968c3b0454c22965 +size 1453738