anusvara handling reworked

This commit is contained in:
minjaesong
2026-03-01 15:10:44 +09:00
parent 5c6da36fa8
commit a69aee9aa7
5 changed files with 164 additions and 116 deletions

View File

@@ -505,12 +505,44 @@ def _generate_devanagari(glyphs, has, replacewith_subs=None):
f" sub {glyph_name(src_cp)} by {targets};" f" sub {glyph_name(src_cp)} by {targets};"
) )
if ccmp_subs or vowel_decomp_subs: # --- ccmp: Contextual Anusvara upper variant ---
ccmp_parts = ["feature ccmp {", " script dev2;"] # 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: if ccmp_subs:
ccmp_parts.append(" lookup DevaConsonantMap {") ccmp_parts.append(" lookup DevaConsonantMap {")
ccmp_parts.extend(" " + s for s in ccmp_subs) ccmp_parts.extend(" " + s for s in ccmp_subs)
ccmp_parts.append(" } DevaConsonantMap;") ccmp_parts.append(" } DevaConsonantMap;")
if anusvara_ccmp_subs:
ccmp_parts.extend(anusvara_ccmp_subs)
if vowel_decomp_subs: if vowel_decomp_subs:
ccmp_parts.append(" lookup DevaVowelDecomp {") ccmp_parts.append(" lookup DevaVowelDecomp {")
ccmp_parts.extend(" " + s for s in vowel_decomp_subs) 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): 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, Returns empty lists for backwards compatibility with the psts assembly.
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).
""" """
anusvara = 0x0902 return [], []
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
def _generate_psts_open_ya(glyphs, has): def _generate_psts_open_ya(glyphs, has):
@@ -1479,70 +1471,118 @@ def _generate_mark(glyphs, has):
def _generate_anusvara_gpos(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 For uF016C (anusvara upper):
shifted right: - complex reph: +3px X, -2px Y
- +3px (+150 units) after uni094F or uF010D (complex reph) - uni094F (or reph + 094F): +3px X
- +2px (+100 units) after uni0948, uni094C, or uF010C (simple reph) - 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 This MUST be appended AFTER _generate_mark() output so its abvm lookups
come after mark-to-base lookups in the LookupList. MarkToBase SETS the come after mark-to-base lookups in the LookupList. MarkToBase SETS the
mark offset; the subsequent SinglePos ADDS to it. 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): has_upper = has(anusvara_upper)
return "" has_regular = has(anusvara)
# +3px triggers: uni094F, complex reph if not has_upper and not has_regular:
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:
return "" return ""
lines = [] lines = []
if shift2_triggers: # --- Lookups for anusvara upper (uF016C) ---
lines.append(f"lookup AnusvaraShift2 {{") if has_upper:
lines.append(f" pos {glyph_name(anusvara_lower)} <100 0 0 0>;") lines.append(f"lookup AnusvaraUpperShift2 {{")
lines.append(f"}} AnusvaraShift2;") lines.append(f" pos {glyph_name(anusvara_upper)} <100 0 0 0>;")
lines.append(f"}} AnusvaraUpperShift2;")
if shift2_up2_triggers: lines.append(f"lookup AnusvaraUpperShift3 {{")
lines.append(f"lookup AnusvaraShift2Up2 {{") lines.append(f" pos {glyph_name(anusvara_upper)} <150 0 0 0>;")
lines.append(f" pos {glyph_name(anusvara_lower)} <100 100 0 0>;") # float up by two pixels. This is a hack lines.append(f"}} AnusvaraUpperShift3;")
lines.append(f"}} AnusvaraShift2Up2;")
if shift3_triggers: lines.append(f"lookup AnusvaraUpperShift3Down2 {{")
lines.append(f"lookup AnusvaraShift3 {{") lines.append(f" pos {glyph_name(anusvara_upper)} <150 -100 0 0>;")
lines.append(f" pos {glyph_name(anusvara_lower)} <150 0 0 0>;") lines.append(f"}} AnusvaraUpperShift3Down2;")
lines.append(f"}} AnusvaraShift3;")
# --- 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("")
lines.append("feature abvm {") lines.append("feature abvm {")
lines.append(" script dev2;") lines.append(" script dev2;")
for cp in shift3_triggers:
lines.append( # --- Rules for anusvara upper (uF016C) ---
f" pos {glyph_name(cp)}" # After reordering: base, [matras], reph?, anusvara.
f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift3;" # When reph is present between matra and anusvara, use 3-glyph backtrack.
) # Rules ordered longest-context-first (first match wins).
for cp in shift2_triggers: if has_upper:
lines.append( # Complex reph → always shift3down2 (directly before anusvara)
f" pos {glyph_name(cp)}" if has(complex_reph):
f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift2;" lines.append(
) f" pos {glyph_name(complex_reph)}"
for cp in shift2_up2_triggers: f" {glyph_name(anusvara_upper)}' lookup AnusvaraUpperShift3Down2;"
lines.append( )
f" pos {glyph_name(cp)}"
f" {glyph_name(anusvara_lower)}' lookup AnusvaraShift2Up2;" # 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;") lines.append("} abvm;")
return '\n'.join(lines) return '\n'.join(lines)

View File

@@ -452,7 +452,7 @@ DEVANAGARI_LIG_J_J_Y = 0xF01AC
MARWARI_LIG_DD_DD = 0xF01BA MARWARI_LIG_DD_DD = 0xF01BA
MARWARI_LIG_DD_DDH = 0xF01BB MARWARI_LIG_DD_DDH = 0xF01BB
DEVANAGARI_ANUSVARA_LOWER = 0xF016C DEVANAGARI_ANUSVARA_UPPER = 0xF016C
MARWARI_LIG_DD_Y = 0xF016E MARWARI_LIG_DD_Y = 0xF016E
MARWARI_HALFLIG_DD_Y = 0xF016F MARWARI_HALFLIG_DD_Y = 0xF016F

Binary file not shown.

View File

@@ -29,7 +29,6 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.* import com.badlogic.gdx.graphics.g2d.*
import com.badlogic.gdx.utils.GdxRuntimeException
import net.torvald.terrarumsansbitmap.DiacriticsAnchor import net.torvald.terrarumsansbitmap.DiacriticsAnchor
import net.torvald.terrarumsansbitmap.GlyphProps import net.torvald.terrarumsansbitmap.GlyphProps
import net.torvald.terrarumsansbitmap.MovableType 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.SHY
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ZWSP import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ZWSP
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.glueCharToGlueSize import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.glueCharToGlueSize
import java.io.BufferedOutputStream
import java.io.FileOutputStream
import java.util.* import java.util.*
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.GZIPInputStream
import kotlin.math.floor
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sign import kotlin.math.sign
@@ -1249,20 +1244,31 @@ class TerrarumSansBitmap(
else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}") else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}")
} }
// Lower Anusvara: shift right when after certain vowels or reph // Upper Anusvara: shift right when after certain vowels or reph
if (thisChar == DEVANAGARI_ANUSVARA_LOWER) { if (thisChar == DEVANAGARI_ANUSVARA_UPPER) {
val prev = str.getOrElse(charIndex - 1) { -1 } val prev = str.getOrElse(charIndex - 1) { -1 }
val hasSimpleReph = prev == DEVANAGARI_RA_SUPER val hasSimpleReph = prev == DEVANAGARI_RA_SUPER
val hasComplexReph = prev == DEVANAGARI_RA_SUPER_COMPLEX val hasComplexReph = prev == DEVANAGARI_RA_SUPER_COMPLEX
val hasReph = hasSimpleReph || hasComplexReph val hasReph = hasSimpleReph || hasComplexReph
val effectivePrev = if (hasReph) str.getOrElse(charIndex - 2) { -1 } else prev 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 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 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 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) seq.add(DEVANAGARI_EYELASH_RA)
i += 1 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 // END of devanagari string replacer
// rearrange {letter, before-and-after diacritics} as {before-diacritics, letter, after-diacritics} // rearrange {letter, before-and-after diacritics} as {before-diacritics, letter, after-diacritics}
else if (glyphProps[c]?.stackWhere == GlyphProps.STACK_BEFORE_N_AFTER) { 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) 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++ i++
} }
@@ -2802,7 +2810,7 @@ class TerrarumSansBitmap(
private const val MARWARI_HALFLIG_DD_Y = 0xF016F private const val MARWARI_HALFLIG_DD_Y = 0xF016F
private const val MARWARI_LIG_DD_R = 0xF010E 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_ING = 0xF0500
private const val SUNDANESE_ENG = 0xF0501 private const val SUNDANESE_ENG = 0xF0501

Binary file not shown.