// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
let indic_feature_tags : Array[@common.Tag] = [
@common.Tag::from_chars('n', 'u', 'k', 't'),
@common.Tag::from_chars('a', 'k', 'h', 'n'),
@common.Tag::from_chars('r', 'p', 'h', 'f'),
@common.Tag::from_chars('r', 'k', 'r', 'f'),
@common.Tag::from_chars('p', 'r', 'e', 'f'),
@common.Tag::from_chars('b', 'l', 'w', 'f'),
@common.Tag::from_chars('a', 'b', 'v', 'f'),
@common.Tag::from_chars('h', 'a', 'l', 'f'),
@common.Tag::from_chars('p', 's', 't', 'f'),
@common.Tag::from_chars('v', 'a', 't', 'u'),
@common.Tag::from_chars('c', 'j', 'c', 't'),
@common.Tag::from_chars('i', 'n', 'i', 't'),
@common.Tag::from_chars('p', 'r', 'e', 's'),
@common.Tag::from_chars('a', 'b', 'v', 's'),
@common.Tag::from_chars('b', 'l', 'w', 's'),
@common.Tag::from_chars('p', 's', 't', 's'),
@common.Tag::from_chars('h', 'a', 'l', 'n'),
]
///|
let indic_cat_c : Int = 1
///|
let indic_cat_cs : Int = 18
///|
let indic_cat_ra : Int = 15
///|
let indic_cat_cm : Int = 16
///|
let indic_cat_repha : Int = 14
///|
let indic_cat_v : Int = 2
///|
let indic_cat_placeholder : Int = 10
///|
let indic_cat_dotted_circle : Int = 11
///|
let indic_cat_h : Int = 4
///|
let indic_cat_zwj : Int = 6
///|
let indic_cat_zwnj : Int = 5
///|
let indic_cat_n : Int = 3
///|
let indic_cat_rs : Int = 12
///|
let indic_cat_mpst : Int = 13
///|
let indic_cat_sm : Int = 8
///|
let indic_pos_start : Int = 0
///|
let indic_pos_pre_c : Int = 3
///|
let indic_pos_base_c : Int = 4
///|
let indic_pos_below_c : Int = 8
///|
let indic_pos_post_c : Int = 11
///|
let indic_pos_smvd : Int = 13
///|
let indic_pos_end : Int = 14
let indic_feature_rphf : Int = 2
let indic_feature_pref : Int = 4
let indic_feature_blwf : Int = 5
let indic_feature_abvf : Int = 6
let indic_feature_half : Int = 7
let indic_feature_pstf : Int = 8
let indic_feature_vatu : Int = 9
let indic_feature_init : Int = 11
fn indic_blwf_pre_and_post(script : @common.Script) -> Bool {
script != @common.script_telugu && script != @common.script_kannada
}
fn indic_reph_requires_zwj(script : @common.Script) -> Bool {
script == @common.script_telugu
}
fn indic_has_old_spec(script : @common.Script) -> Bool {
script == @common.script_devanagari ||
script == @common.script_bengali ||
script == @common.script_gurmukhi ||
script == @common.script_gujarati ||
script == @common.script_oriya ||
script == @common.script_tamil ||
script == @common.script_telugu ||
script == @common.script_kannada ||
script == @common.script_malayalam
}
fn indic_script_tag_2(script_tag : @common.Tag) -> @common.Tag {
let (b1, b2, b3, _) = script_tag.bytes()
@common.Tag::from_bytes(b1, b2, b3, b'2')
}
fn indic_script_tag_3(script_tag : @common.Tag) -> @common.Tag {
let (b1, b2, b3, _) = script_tag.bytes()
@common.Tag::from_bytes(b1, b2, b3, b'3')
}
fn indic_has_consonant_after(
categories : Array[Int],
start : Int,
end : Int,
) -> Bool {
if start < 0 || end > categories.length() || start >= end {
return false
}
for i in start.. Int {
if start < 0 || end > categories.length() || start >= end {
return start
}
if script == @common.script_malayalam &&
categories[start] == indic_cat_repha {
let mut limit = start + 1
while limit < end &&
(categories[limit] == indic_cat_zwj || categories[limit] == indic_cat_zwnj) {
limit = limit + 1
}
if !indic_has_consonant_after(categories, limit, end) {
return start
}
return limit
}
if !has_rphf || start + 2 >= end {
return start
}
if categories[start] != indic_cat_ra || categories[start + 1] != indic_cat_h {
return start
}
let mut limit = start + 2
if indic_reph_requires_zwj(script) {
if start + 2 >= end || categories[start + 2] != indic_cat_zwj {
return start
}
limit = start + 3
}
while limit < end &&
(categories[limit] == indic_cat_zwj || categories[limit] == indic_cat_zwnj) {
limit = limit + 1
}
if !indic_has_consonant_after(categories, limit, end) {
return start
}
limit
}
fn indic_virama_codepoint(script : @common.Script) -> UInt? {
if script == @common.script_devanagari {
return Some(0x094DU)
}
if script == @common.script_bengali {
return Some(0x09CDU)
}
if script == @common.script_gurmukhi {
return Some(0x0A4DU)
}
if script == @common.script_gujarati {
return Some(0x0ACDU)
}
if script == @common.script_oriya {
return Some(0x0B4DU)
}
if script == @common.script_tamil {
return Some(0x0BCDU)
}
if script == @common.script_telugu {
return Some(0x0C4DU)
}
if script == @common.script_kannada {
return Some(0x0CCDU)
}
if script == @common.script_malayalam {
return Some(0x0D4DU)
}
None
}
fn is_consonant_indic(category : Int) -> Bool {
category == indic_cat_c ||
category == indic_cat_cs ||
category == indic_cat_ra ||
category == indic_cat_cm ||
category == indic_cat_v ||
category == indic_cat_placeholder ||
category == indic_cat_dotted_circle
}
fn is_joiner_or_mark_indic(category : Int) -> Bool {
category == indic_cat_zwj ||
category == indic_cat_zwnj ||
category == indic_cat_n ||
category == indic_cat_rs ||
category == indic_cat_cm ||
category == indic_cat_h
}
fn reorder_range_by_positions_indic(
buffer : Buffer,
categories : Array[Int],
positions : Array[Int],
start : Int,
end : Int,
) -> (Array[Int], Array[Int]) {
if start < 0 || end > buffer.infos.length() || start >= end {
return (categories, positions)
}
let range_len = end - start
let order : Array[Int] = []
for pos in indic_pos_start..=indic_pos_end {
for rel in 0.. Unit {
if categories.length() != buffer.infos.length() ||
positions.length() != buffer.infos.length() ||
syllables.length() != buffer.infos.length() {
return ()
}
let use_reph = glyphs.length() == categories.length() &&
!rphf_lookups.is_empty()
let reph_requires_zwj = indic_reph_requires_zwj(script)
let mut current_categories = categories
let mut current_positions = positions
let mut i = 0
while i < syllables.length() {
let syllable = syllables[i]
let mut j = i + 1
while j < syllables.length() && syllables[j] == syllable {
j = j + 1
}
buffer.merge_clusters(i, j)
let mut has_reph = false
if use_reph &&
i + 1 < j &&
current_categories[i] == indic_cat_ra &&
current_categories[i + 1] == indic_cat_h {
let mut reph_len = 2
if reph_requires_zwj {
if i + 2 < j && current_categories[i + 2] == indic_cat_zwj {
reph_len = 3
} else {
reph_len = 0
}
}
if reph_len > 0 &&
indic_would_substitute(gsub, rphf_lookups, glyphs, i, reph_len) {
let limit = indic_reph_limit(script, current_categories, i, j, true)
if limit > i {
has_reph = true
}
}
}
let reph_limit = indic_reph_limit(script, current_categories, i, j, has_reph)
let base = find_base_indic_with_limit(
current_categories,
current_positions,
i,
j,
reph_limit,
)
for k in i.. indic_pos_pre_c {
current_positions[k] = indic_pos_pre_c
}
}
if base < j {
current_positions[base] = indic_pos_base_c
}
let mut last_pos = indic_pos_start
for k in i.. i {
if current_positions[prev - 1] != indic_pos_smvd {
current_positions[k] = current_positions[prev - 1]
break
}
prev = prev - 1
}
}
} else if current_positions[k] != indic_pos_smvd {
if current_categories[k] == indic_cat_mpst &&
k > i &&
current_categories[k - 1] == indic_cat_sm {
current_positions[k - 1] = current_positions[k]
}
last_pos = current_positions[k]
}
}
let (next_categories, next_positions) = reorder_range_by_positions_indic(
buffer,
current_categories,
current_positions,
i,
j,
)
current_categories = next_categories
current_positions = next_positions
i = j
}
}
fn find_base_indic_with_limit(
categories : Array[Int],
positions : Array[Int],
start : Int,
end : Int,
limit : Int,
) -> Int {
if start < 0 || end > categories.length() || start >= end {
return start
}
let search_start = if limit < start { start } else { limit }
if search_start >= end {
return start
}
let mut base = end
let mut seen_below = false
let mut k = end
while k > search_start {
k = k - 1
if is_consonant_indic(categories[k]) {
if positions[k] != indic_pos_below_c &&
(positions[k] != indic_pos_post_c || seen_below) {
return k
}
if positions[k] == indic_pos_below_c {
seen_below = true
}
base = k
} else if k > search_start &&
categories[k] == indic_cat_zwj &&
categories[k - 1] == indic_cat_h {
break
}
}
if base < end {
return base
}
search_start
}
fn indic_would_substitute(
gsub : @ot_tables.GsubTable?,
lookup_offsets : Array[Int],
glyphs : Array[UInt],
start : Int,
length : Int,
) -> Bool {
if lookup_offsets.is_empty() || length <= 0 || start < 0 ||
start + length > glyphs.length() {
return false
}
let gsub = match gsub {
None => return false
Some(value) => value
}
let sub_glyphs : Array[UInt] = []
for i in start..<(start + length) {
sub_glyphs.push(glyphs[i])
}
let sub_clusters = Array::make(length, 0)
let sub_masks = Array::make(length, 1U)
match gsub.apply_with_lookups(
sub_glyphs,
sub_clusters,
lookup_offsets[:],
masks=sub_masks,
lookup_mask=1U,
) {
Err(_) => false
Ok((_, _, changed)) => changed
}
}
fn build_indic_feature_masks(
script : @common.Script,
is_old_spec : Bool,
categories : Array[Int],
positions : Array[Int],
syllables : Array[Int],
glyphs : Array[UInt],
gsub : @ot_tables.GsubTable?,
rphf_lookups : Array[Int],
pref_lookups : Array[Int],
) -> Array[UInt] {
let len = categories.length()
let masks = Array::make(len, 0U)
let rphf_bit = 1U << indic_feature_rphf
let pref_bit = 1U << indic_feature_pref
let blwf_bit = 1U << indic_feature_blwf
let abvf_bit = 1U << indic_feature_abvf
let half_bit = 1U << indic_feature_half
let pstf_bit = 1U << indic_feature_pstf
let init_bit = 1U << indic_feature_init
let blwf_pre_and_post = indic_blwf_pre_and_post(script)
let reph_requires_zwj = indic_reph_requires_zwj(script)
let mut i = 0
while i < syllables.length() {
let syllable = syllables[i]
let mut j = i + 1
while j < syllables.length() && syllables[j] == syllable {
j = j + 1
}
let mut has_reph = false
if i + 1 < j &&
categories[i] == indic_cat_ra &&
categories[i + 1] == indic_cat_h {
let mut reph_len = 2
if reph_requires_zwj {
if i + 2 < j && categories[i + 2] == indic_cat_zwj {
reph_len = 3
} else {
reph_len = 0
}
}
if reph_len > 0 &&
indic_would_substitute(gsub, rphf_lookups, glyphs, i, reph_len) {
let limit = indic_reph_limit(script, categories, i, j, true)
if limit > i {
has_reph = true
masks[i] = masks[i] | rphf_bit
}
}
}
let reph_limit = indic_reph_limit(script, categories, i, j, has_reph)
let base = find_base_indic_with_limit(categories, positions, i, j, reph_limit)
let mut pre_mask = half_bit
if blwf_pre_and_post {
pre_mask = pre_mask | blwf_bit
}
for k in i..= i && base < j {
masks[base] = masks[base] | init_bit
}
for k in (base + 1)..= i {
if is_consonant_indic(categories[p]) {
masks[p] = masks[p] & (0xFFFFFFFFU - half_bit)
break
}
if p == 0 {
break
}
p = p - 1
}
}
z = z + 1
}
i = j
}
masks
}
fn update_indic_positions_from_face(
positions : Array[Int],
glyphs : Array[UInt],
gsub : @ot_tables.GsubTable?,
virama_glyph : UInt,
blwf_lookups : Array[Int],
pstf_lookups : Array[Int],
vatu_lookups : Array[Int],
pref_lookups : Array[Int],
) -> Unit {
if positions.length() != glyphs.length() {
return ()
}
if virama_glyph == 0U {
return ()
}
if blwf_lookups.is_empty() &&
pstf_lookups.is_empty() &&
vatu_lookups.is_empty() &&
pref_lookups.is_empty() {
return ()
}
for i in 0.. Result[Unit, ShapeError] {
let gsub = match font.gsub() {
Err(err) => return Err(Font(err))
Ok(value) => value
}
let script_tag = self.script.to_tag()
let alt_script_tag3 = indic_script_tag_3(script_tag)
let alt_script_tag = indic_script_tag_2(script_tag)
let mut selected_script_tag = script_tag
let mut is_old_spec = indic_has_old_spec(self.script)
let lang_tag = language_to_ot_tag(self.language)
if gsub is Some(gsub) {
let dflt_tag = @common.Tag::from_chars('D', 'F', 'L', 'T')
let latn_tag = @common.Tag::from_chars('l', 'a', 't', 'n')
if buffer_layout_has_script(gsub.layout(), alt_script_tag3) {
return self.shape_use(font, script_tag=alt_script_tag3)
}
let has_script = buffer_layout_has_script(gsub.layout(), script_tag) ||
buffer_layout_has_script(gsub.layout(), alt_script_tag)
if buffer_layout_has_script(gsub.layout(), alt_script_tag) {
selected_script_tag = alt_script_tag
is_old_spec = false
}
if !has_script &&
(buffer_layout_has_script(gsub.layout(), dflt_tag) ||
buffer_layout_has_script(gsub.layout(), latn_tag)) {
return self.shape_ot_with_options(
font,
fallback_position=false,
zero_width_marks=ZeroWidthMarks::Disabled,
)
}
}
preprocess_vowel_constraints(self)
let categories : Array[UInt] = []
let categories_i : Array[Int] = []
for info in self.infos {
let category = indic_category(indic_get_categories(info.codepoint))
categories.push(category)
categories_i.push(category.reinterpret_as_int())
}
let syllables = find_syllables_indic(categories)
match insert_dotted_circle_for_syllables_with_repha(
font,
self,
syllables,
indic_syllable_type_broken_cluster,
categories_i,
indic_cat_repha,
) {
Err(err) => return Err(err)
Ok(_) => ()
}
let categories_after_u : Array[UInt] = []
let categories_after_i : Array[Int] = []
let positions_after : Array[Int] = []
for info in self.infos {
let packed = indic_get_categories(info.codepoint)
categories_after_u.push(indic_category(packed))
categories_after_i.push(indic_category(packed).reinterpret_as_int())
positions_after.push(indic_position(packed))
}
let syllables_after = find_syllables_indic(categories_after_u)
let mut rphf_lookups : Array[Int] = []
let mut pref_lookups : Array[Int] = []
let mut blwf_lookups : Array[Int] = []
let mut pstf_lookups : Array[Int] = []
let mut vatu_lookups : Array[Int] = []
if gsub is Some(gsub) {
rphf_lookups = match gsub.layout().select_lookup_offsets_for_feature(
selected_script_tag,
indic_feature_tags[indic_feature_rphf],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
pref_lookups = match gsub.layout().select_lookup_offsets_for_feature(
selected_script_tag,
indic_feature_tags[indic_feature_pref],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
blwf_lookups = match gsub.layout().select_lookup_offsets_for_feature(
selected_script_tag,
indic_feature_tags[indic_feature_blwf],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
pstf_lookups = match gsub.layout().select_lookup_offsets_for_feature(
selected_script_tag,
indic_feature_tags[indic_feature_pstf],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
vatu_lookups = match gsub.layout().select_lookup_offsets_for_feature(
selected_script_tag,
indic_feature_tags[indic_feature_vatu],
language_tag=lang_tag,
include_required=false,
) {
Err(err) => return Err(Layout(err))
Ok(value) => value
}
}
let glyphs_for_positions : Array[UInt] = []
for info in self.infos {
let glyph = match font.glyph_for_codepoint(info.codepoint) {
Err(err) => return Err(Font(err))
Ok(None) => 0U
Ok(Some(value)) => value
}
glyphs_for_positions.push(glyph)
}
match indic_virama_codepoint(self.script) {
None => ()
Some(virama_codepoint) => {
let virama_glyph = match font.glyph_for_codepoint(virama_codepoint) {
Err(err) => return Err(Font(err))
Ok(None) => 0U
Ok(Some(value)) => value
}
update_indic_positions_from_face(
positions_after,
glyphs_for_positions,
gsub,
virama_glyph,
blwf_lookups,
pstf_lookups,
vatu_lookups,
pref_lookups,
)
}
}
reorder_indic_syllables(
self,
self.script,
categories_after_i,
positions_after,
syllables_after,
gsub,
glyphs_for_positions,
rphf_lookups,
)
let reordered_categories : Array[Int] = []
let reordered_positions : Array[Int] = []
for info in self.infos {
let packed = indic_get_categories(info.codepoint)
reordered_categories.push(indic_category(packed).reinterpret_as_int())
reordered_positions.push(indic_position(packed))
}
match self.shape_basic(font) {
Err(err) => return Err(err)
Ok(_) => ()
}
let glyphs : Array[UInt] = []
for info in self.infos {
glyphs.push(info.codepoint)
}
let glyph_masks = build_indic_feature_masks(
self.script,
is_old_spec,
reordered_categories,
reordered_positions,
syllables_after,
glyphs,
gsub,
rphf_lookups,
pref_lookups,
)
if gsub is Some(gsub) {
match apply_gsub_features_for_syllables(
font,
self,
gsub,
indic_feature_tags,
syllables_after,
glyph_masks=glyph_masks,
script_tag=selected_script_tag,
) {
Err(err) => return Err(err)
Ok(_) => ()
}
}
apply_gpos(font, self)
}