use crate::face::Face;
use crate::shaper::{shape_run_with_font, PositionedGlyph};
use crate::shaping::arabic::{compute_forms, script_of, Script};
use crate::shaping::arabic_pf::presentation_form;
use crate::shaping::indic::{
cluster_boundaries_with, reorder_cluster_with, script_indic_tags, ReorderRules, BENGALI_RULES,
DEVANAGARI_RULES, TAMIL_RULES,
};
use crate::style::Style;
use crate::Error;
#[derive(Debug)]
pub struct FaceChain {
faces: Vec<Face>,
}
impl FaceChain {
pub fn new(primary: Face) -> Self {
Self {
faces: vec![primary],
}
}
#[must_use]
pub fn push_fallback(mut self, face: Face) -> Self {
self.faces.push(face);
self
}
pub fn len(&self) -> usize {
self.faces.len()
}
pub fn is_empty(&self) -> bool {
self.faces.is_empty()
}
pub fn face(&self, idx: u16) -> &Face {
&self.faces[idx as usize]
}
pub fn primary(&self) -> &Face {
&self.faces[0]
}
pub fn face_mut(&mut self, idx: usize) -> &mut Face {
&mut self.faces[idx]
}
pub fn set_variation_coords(&mut self, coords: &[f32]) -> Result<(), Error> {
self.faces[0].set_variation_coords(coords)
}
pub fn named_instances(&self, face_index: usize) -> Vec<crate::NamedInstance> {
self.faces
.get(face_index)
.map(|f| f.named_instances())
.unwrap_or_default()
}
pub fn variation_axes(&self, face_index: usize) -> Vec<crate::VariationAxis> {
self.faces
.get(face_index)
.map(|f| f.variation_axes())
.unwrap_or_default()
}
pub fn shape(&self, text: &str, size_px: f32) -> Result<Vec<PositionedGlyph>, Error> {
self.shape_styled(text, size_px, Style::REGULAR)
}
pub fn shape_styled(
&self,
text: &str,
size_px: f32,
_style: Style,
) -> Result<Vec<PositionedGlyph>, Error> {
if text.is_empty() || size_px <= 0.0 {
return Ok(Vec::new());
}
let assigned = self.assign_codepoints(text)?;
if assigned.is_empty() {
return Ok(Vec::new());
}
let mut out: Vec<PositionedGlyph> = Vec::with_capacity(assigned.len());
let mut run_start = 0usize;
while run_start < assigned.len() {
let face_idx = assigned[run_start].0;
let mut run_end = run_start + 1;
while run_end < assigned.len() && assigned[run_end].0 == face_idx {
run_end += 1;
}
let gids: Vec<u16> = assigned[run_start..run_end].iter().map(|p| p.1).collect();
let face = &self.faces[face_idx as usize];
let mut run_glyphs =
face.with_font(|font| shape_run_with_font(font, &gids, size_px, face_idx))?;
out.append(&mut run_glyphs);
run_start = run_end;
}
Ok(out)
}
fn assign_codepoints(&self, text: &str) -> Result<Vec<(u16, u16)>, Error> {
let chars: Vec<char> = text.chars().collect();
let arabic_shaped = apply_arabic_joining(&chars);
let (shaped_chars, reph_marks) = apply_indic_reorder(&arabic_shaped);
let mut out: Vec<(u16, u16)> = Vec::with_capacity(shaped_chars.len());
for (orig_idx, ch) in shaped_chars.iter().enumerate() {
let mut found: Option<(u16, u16)> = None;
for (idx, face) in self.faces.iter().enumerate() {
let g = face.with_font(|font| font.glyph_index(*ch))?;
match g {
Some(gid) if gid != 0 => {
found = Some((idx as u16, gid));
break;
}
_ => continue,
}
}
if found.is_none() {
let orig = if orig_idx < chars.len() && *ch != chars[orig_idx] {
Some(chars[orig_idx])
} else if !chars.contains(ch) {
None
} else {
None
};
if let Some(orig) = orig {
for (idx, face) in self.faces.iter().enumerate() {
let g = face.with_font(|font| font.glyph_index(orig))?;
if let Some(gid) = g {
if gid != 0 {
found = Some((idx as u16, gid));
break;
}
}
}
}
}
out.push(found.unwrap_or((0, 0)));
}
let out = self.apply_reph_substitutions(out, &reph_marks)?;
Ok(out)
}
fn apply_reph_substitutions(
&self,
glyphs: Vec<(u16, u16)>,
marks: &[RephMark],
) -> Result<Vec<(u16, u16)>, Error> {
if marks.is_empty() {
return Ok(glyphs);
}
let mut out = glyphs;
for mark in marks.iter().rev() {
if mark.ra_idx >= out.len() || mark.halant_idx >= out.len() {
continue;
}
let (ra_face_idx, ra_gid) = out[mark.ra_idx];
if ra_gid == 0 {
continue;
}
let face = &self.faces[ra_face_idx as usize];
let (modern, legacy) = match script_indic_tags(mark.script) {
Some(p) => p,
None => continue,
};
let new_ra = face.with_font(|font| {
let mut substitute: Option<u16> = None;
for tag in [modern, legacy] {
let features = font.gsub_features_for_script(tag, None);
for feat in features {
if feat.tag == *b"rphf" {
for &lookup_idx in &feat.lookup_indices {
if let Some(g) = font.gsub_apply_lookup_type_1(lookup_idx, ra_gid) {
substitute = Some(g);
break;
}
}
if substitute.is_some() {
break;
}
}
}
if substitute.is_some() {
break;
}
}
substitute
})?;
if let Some(reph_gid) = new_ra {
out[mark.ra_idx] = (ra_face_idx, reph_gid);
if mark.halant_idx < out.len() {
out.remove(mark.halant_idx);
}
}
}
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RephMark {
ra_idx: usize,
halant_idx: usize,
script: Script,
}
fn apply_indic_reorder(chars: &[char]) -> (Vec<char>, Vec<RephMark>) {
if chars.is_empty() {
return (Vec::new(), Vec::new());
}
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut reph_marks: Vec<RephMark> = Vec::new();
let mut i = 0;
while i < chars.len() {
let run_script = script_of(chars[i]);
let rules = match indic_rules_for_script(run_script) {
Some(r) => r,
None => {
out.push(chars[i]);
i += 1;
continue;
}
};
let run_start = i;
while i < chars.len() && script_of(chars[i]) == run_script {
i += 1;
}
let run = &chars[run_start..i];
let bounds = cluster_boundaries_with(run, rules.category);
for (s, e) in bounds {
let cluster = &run[s..e];
let (reordered, flags) = reorder_cluster_with(cluster, rules);
if flags.has_reph {
let ra_offset = if flags.pre_base_reordered { 1 } else { 0 };
let ra_idx = out.len() + ra_offset;
let halant_idx = ra_idx + 1;
reph_marks.push(RephMark {
ra_idx,
halant_idx,
script: run_script,
});
}
out.extend_from_slice(&reordered);
}
}
(out, reph_marks)
}
fn indic_rules_for_script(script: Script) -> Option<&'static ReorderRules> {
match script {
Script::Devanagari => Some(&DEVANAGARI_RULES),
Script::Bengali => Some(&BENGALI_RULES),
Script::Tamil => Some(&TAMIL_RULES),
_ => None,
}
}
fn apply_arabic_joining(chars: &[char]) -> Vec<char> {
if chars.is_empty() {
return Vec::new();
}
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut i = 0;
while i < chars.len() {
let run_start = i;
while i < chars.len() && script_of(chars[i]) == Script::Arabic {
i += 1;
}
if i > run_start {
let run = &chars[run_start..i];
let forms = compute_forms(run);
for (k, &ch) in run.iter().enumerate() {
let translated = presentation_form(ch, forms[k]).unwrap_or(ch);
out.push(translated);
}
}
if i < chars.len() && script_of(chars[i]) != Script::Arabic {
out.push(chars[i]);
i += 1;
}
}
out
}
#[cfg(test)]
#[allow(non_snake_case)] mod tests {
use super::{apply_arabic_joining, apply_indic_reorder};
use crate::shaping::arabic::Script;
#[test]
fn ascii_passes_through_unchanged() {
let chars: Vec<char> = "Hello".chars().collect();
assert_eq!(apply_arabic_joining(&chars), chars);
}
#[test]
fn devanagari_pre_base_matra_moves_to_front_of_cluster() {
let chars = vec!['\u{0915}', '\u{093F}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}']);
assert!(marks.is_empty(), "no reph in this cluster");
}
#[test]
fn devanagari_two_clusters_each_reorder_independently() {
let chars = vec!['\u{0915}', '\u{093F}', '\u{0915}', '\u{093F}'];
let (out, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{093F}', '\u{0915}']);
}
#[test]
fn devanagari_conjunct_reorder_keeps_halant_chain_intact() {
let chars = vec!['\u{0915}', '\u{094D}', '\u{0937}', '\u{093F}'];
let (out, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{094D}', '\u{0937}']);
}
#[test]
fn ascii_passes_through_indic_reorder_unchanged() {
let chars: Vec<char> = "Hello".chars().collect();
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, chars);
assert!(marks.is_empty());
}
#[test]
fn mixed_latin_and_devanagari_reorders_only_devanagari_clusters() {
let chars = vec!['A', '\u{0915}', '\u{093F}'];
let (out, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['A', '\u{093F}', '\u{0915}']);
}
#[test]
fn devanagari_reph_emits_reph_mark_at_correct_index() {
let chars = vec!['\u{0930}', '\u{094D}', '\u{0915}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0930}', '\u{094D}', '\u{0915}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].ra_idx, 0);
assert_eq!(marks[0].halant_idx, 1);
assert_eq!(marks[0].script, Script::Devanagari);
}
#[test]
fn devanagari_reph_with_pre_base_matra_shifts_reph_mark_by_one() {
let chars = vec!['\u{0930}', '\u{094D}', '\u{0915}', '\u{093F}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0930}', '\u{094D}', '\u{0915}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].ra_idx, 1);
assert_eq!(marks[0].halant_idx, 2);
}
#[test]
fn bengali_pre_base_matra_e_moves_to_front_of_cluster() {
let chars = vec!['\u{0995}', '\u{09C7}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{09C7}', '\u{0995}']);
assert!(marks.is_empty());
}
#[test]
fn bengali_reph_emits_reph_mark_with_bengali_script_tag() {
let chars = vec!['\u{09B0}', '\u{09CD}', '\u{0995}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{09B0}', '\u{09CD}', '\u{0995}']);
assert_eq!(marks.len(), 1);
assert_eq!(marks[0].script, Script::Bengali);
}
#[test]
fn tamil_pre_base_matra_e_moves_to_front_of_cluster() {
let chars = vec!['\u{0B95}', '\u{0BC6}'];
let (out, marks) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{0BC6}', '\u{0B95}']);
assert!(marks.is_empty());
}
#[test]
fn tamil_RA_plus_halant_does_NOT_emit_reph_mark() {
let chars = vec!['\u{0BB0}', '\u{0BCD}', '\u{0B95}'];
let (_out, marks) = apply_indic_reorder(&chars);
assert!(marks.is_empty(), "Tamil never sets the reph flag");
}
#[test]
fn mixed_devanagari_and_bengali_runs_segment_independently() {
let chars = vec!['\u{0915}', '\u{093F}', '\u{0995}', '\u{09BF}'];
let (out, _) = apply_indic_reorder(&chars);
assert_eq!(out, vec!['\u{093F}', '\u{0915}', '\u{09BF}', '\u{0995}']);
}
#[test]
fn arabic_run_translates_to_presentation_forms() {
let chars = vec!['\u{0628}', '\u{0628}', '\u{0628}'];
let out = apply_arabic_joining(&chars);
assert_eq!(out, vec!['\u{FE91}', '\u{FE92}', '\u{FE90}']);
}
#[test]
fn arabic_run_with_ascii_separator() {
let chars = vec!['\u{0628}', '\u{0628}', ' ', '\u{0628}', '\u{0628}'];
let out = apply_arabic_joining(&chars);
assert_eq!(
out,
vec!['\u{FE91}', '\u{FE90}', ' ', '\u{FE91}', '\u{FE90}']
);
}
}