mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-07 11:51:50 +09:00
contextual devanagari anusvara positioning
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user