Skip to main content

harfrust/hb/
ot_shaper_indic.rs

1use alloc::boxed::Box;
2use core::cmp;
3use core::ops::Range;
4
5use read_fonts::types::GlyphId;
6
7use super::algs::*;
8use super::buffer::*;
9use super::font_funcs::FontFuncsDispatch;
10use super::ot_layout::*;
11use super::ot_layout_gsubgpos::WouldApplyContext;
12use super::ot_map::*;
13use super::ot_shape::*;
14use super::ot_shape_normalize::*;
15use super::ot_shape_plan::hb_ot_shape_plan_t;
16use super::ot_shaper::*;
17use super::ot_shaper_syllabic::*;
18use super::unicode::Codepoint;
19use super::unicode::{hb_gc, CharExt};
20use super::{hb_font_t, hb_mask_t, hb_tag_t, script, GlyphInfo, Script};
21
22pub const INDIC_SHAPER: hb_ot_shaper_t = hb_ot_shaper_t {
23    collect_features: Some(collect_features),
24    override_features: Some(override_features),
25    create_data: Some(|plan| Box::new(IndicShapePlan::new(plan))),
26    preprocess_text: Some(preprocess_text),
27    postprocess_glyphs: None,
28    normalization_preference: HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS_NO_SHORT_CIRCUIT,
29    decompose: Some(decompose),
30    compose: Some(compose),
31    setup_masks: Some(setup_masks),
32    gpos_tag: None,
33    reorder_marks: None,
34    zero_width_marks: HB_OT_SHAPE_ZERO_WIDTH_MARKS_NONE,
35    fallback_position: false,
36};
37
38impl GlyphInfo {
39    declare_buffer_var_alias!(
40        OT_SHAPER_VAR_U8_CATEGORY_VAR,
41        u8,
42        INDIC_CATEGORY_VAR,
43        indic_category,
44        set_indic_category
45    );
46    declare_buffer_var_alias!(
47        OT_SHAPER_VAR_U8_AUXILIARY_VAR,
48        u8,
49        INDIC_POSITION_VAR,
50        indic_position,
51        set_indic_position
52    );
53
54    fn is_one_of(&self, flags: u32) -> bool {
55        // If it ligated, all bets are off.
56        if self.ligated() {
57            return false;
58        }
59
60        rb_flag_unsafe(self.indic_category() as u32) & flags != 0
61    }
62
63    fn is_joiner(&self) -> bool {
64        self.is_one_of(JOINER_FLAGS)
65    }
66
67    pub(crate) fn is_consonant(&self) -> bool {
68        self.is_one_of(CONSONANT_FLAGS_INDIC)
69    }
70
71    fn is_halant(&self) -> bool {
72        self.is_one_of(rb_flag(ot_category_t::OT_H as u32))
73    }
74
75    fn set_indic_properties(&mut self) {
76        let u = self.glyph_id;
77        let (cat, pos) = crate::hb::ot_shaper_indic_table::get_categories(u);
78
79        self.set_indic_category(cat);
80        self.set_indic_position(pos);
81    }
82}
83
84pub type Category = u8;
85
86// This mod doesn't exist in harfbuzz anymore. Instead, the corresponding values are auto-generated
87// by the various machines and stored in `hb-ot-shaper-indic-table`. This means that when updating the
88// values in the machines, we also need to update them here.
89#[allow(dead_code)]
90pub mod ot_category_t {
91    pub const OT_X: u8 = 0;
92    pub const OT_C: u8 = 1;
93    pub const OT_V: u8 = 2;
94    pub const OT_N: u8 = 3;
95    pub const OT_H: u8 = 4;
96    pub const OT_ZWNJ: u8 = 5;
97    pub const OT_ZWJ: u8 = 6;
98    pub const OT_M: u8 = 7;
99    pub const OT_SM: u8 = 8;
100    pub const OT_A: u8 = 9;
101    pub const OT_VD: u8 = OT_A;
102    pub const OT_PLACEHOLDER: u8 = 10;
103    pub const OT_GB: u8 = OT_PLACEHOLDER;
104    pub const OT_DOTTEDCIRCLE: u8 = 11;
105    pub const OT_RS: u8 = 12; // Register Shifter, used in Khmer OT spec.
106    pub const OT_MPst: u8 = 13;
107    pub const OT_Repha: u8 = 14; // Atomically-encoded logical or visual repha.
108    pub const OT_Ra: u8 = 15;
109    pub const OT_CM: u8 = 16; // Consonant-Medial.
110    pub const OT_Symbol: u8 = 17; // Avagraha, etc that take marks (SM,A,VD).
111    pub const OT_CS: u8 = 18;
112
113    /* Khmer & Myanmar shapers. */
114    pub const OT_VAbv: u8 = 20;
115    pub const OT_VBlw: u8 = 21;
116    pub const OT_VPre: u8 = 22;
117    pub const OT_VPst: u8 = 23;
118
119    /* Khmer. */
120    pub const OT_Robatic: u8 = 25;
121    pub const OT_Xgroup: u8 = 26;
122    pub const OT_Ygroup: u8 = 27;
123
124    /* Myanmar */
125    pub const OT_As: u8 = 32; // Asat
126    pub const OT_MH: u8 = 35; // Medial
127    pub const OT_MR: u8 = 36; // Medial
128    pub const OT_MW: u8 = 37; // Medial
129    pub const OT_MY: u8 = 38; // Medial
130    pub const OT_PT: u8 = 39; // Pwo and other tones
131    pub const OT_VS: u8 = 40; // Variation selectors
132    pub const OT_ML: u8 = 41; // Consonant medials
133
134    pub const OT_SMPst: u8 = 57; // Syllable Medial Post-base
135
136    // This one doesn't exist in ot_category_t in harfbuzz, only in
137    // the Myanmar machine. However, in Rust we unfortunately can't export
138    // inside the Ragel file, so we have to define it here as well. Needs to
139    // be kept in sync with the value in the machine.
140    pub const IV: u8 = 2;
141}
142
143pub mod ot_position_t {
144    pub const POS_START: u8 = 0;
145
146    pub const POS_RA_TO_BECOME_REPH: u8 = 1;
147    pub const POS_PRE_M: u8 = 2;
148    pub const POS_PRE_C: u8 = 3;
149
150    pub const POS_BASE_C: u8 = 4;
151    pub const POS_AFTER_MAIN: u8 = 5;
152
153    pub const POS_ABOVE_C: u8 = 6;
154
155    pub const POS_BEFORE_SUB: u8 = 7;
156    pub const POS_BELOW_C: u8 = 8;
157    pub const POS_AFTER_SUB: u8 = 9;
158
159    pub const POS_BEFORE_POST: u8 = 10;
160    pub const POS_POST_C: u8 = 11;
161    pub const POS_AFTER_POST: u8 = 12;
162
163    pub const POS_SMVD: u8 = 13;
164
165    pub const POS_END: u8 = 14;
166}
167
168const INDIC_FEATURES: &[(hb_tag_t, hb_ot_map_feature_flags_t)] = &[
169    // Basic features.
170    // These features are applied in order, one at a time, after initial_reordering,
171    // constrained to the syllable.
172    (
173        hb_tag_t::new(b"nukt"),
174        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
175    ),
176    (
177        hb_tag_t::new(b"akhn"),
178        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
179    ),
180    (hb_tag_t::new(b"rphf"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
181    (
182        hb_tag_t::new(b"rkrf"),
183        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
184    ),
185    (hb_tag_t::new(b"pref"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
186    (hb_tag_t::new(b"blwf"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
187    (hb_tag_t::new(b"abvf"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
188    (hb_tag_t::new(b"half"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
189    (hb_tag_t::new(b"pstf"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
190    (
191        hb_tag_t::new(b"vatu"),
192        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
193    ),
194    (
195        hb_tag_t::new(b"cjct"),
196        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
197    ),
198    // Other features.
199    // These features are applied all at once, after final_reordering, constrained
200    // to the syllable.
201    // Default Bengali font in Windows for example has intermixed
202    // lookups for init,pres,abvs,blws features.
203    (hb_tag_t::new(b"init"), F_MANUAL_JOINERS | F_PER_SYLLABLE),
204    (
205        hb_tag_t::new(b"pres"),
206        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
207    ),
208    (
209        hb_tag_t::new(b"abvs"),
210        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
211    ),
212    (
213        hb_tag_t::new(b"blws"),
214        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
215    ),
216    (
217        hb_tag_t::new(b"psts"),
218        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
219    ),
220    (
221        hb_tag_t::new(b"haln"),
222        F_GLOBAL_MANUAL_JOINERS | F_PER_SYLLABLE,
223    ),
224];
225
226// Must be in the same order as the INDIC_FEATURES array.
227#[allow(dead_code)]
228mod indic_feature {
229    pub const NUKT: usize = 0;
230    pub const AKHN: usize = 1;
231    pub const RPHF: usize = 2;
232    pub const RKRF: usize = 3;
233    pub const PREF: usize = 4;
234    pub const BLWF: usize = 5;
235    pub const ABVF: usize = 6;
236    pub const HALF: usize = 7;
237    pub const PSTF: usize = 8;
238    pub const VATU: usize = 9;
239    pub const CJCT: usize = 10;
240    pub const INIT: usize = 11;
241    pub const PRES: usize = 12;
242    pub const ABVS: usize = 13;
243    pub const BLWS: usize = 14;
244    pub const PSTS: usize = 15;
245    pub const HALN: usize = 16;
246}
247
248pub(crate) const fn category_flag(c: Category) -> u32 {
249    rb_flag(c as u32)
250}
251
252// Note:
253//
254// We treat Vowels and placeholders as if they were consonants.  This is safe because Vowels
255// cannot happen in a consonant syllable.  The plus side however is, we can call the
256// consonant syllable logic from the vowel syllable function and get it all right!
257const CONSONANT_FLAGS_INDIC: u32 = category_flag(ot_category_t::OT_C)
258    | category_flag(ot_category_t::OT_CS)
259    | category_flag(ot_category_t::OT_Ra)
260    | category_flag(ot_category_t::OT_CM)
261    | category_flag(ot_category_t::OT_V)
262    | category_flag(ot_category_t::OT_PLACEHOLDER)
263    | category_flag(ot_category_t::OT_DOTTEDCIRCLE);
264
265const JOINER_FLAGS: u32 =
266    category_flag(ot_category_t::OT_ZWJ) | category_flag(ot_category_t::OT_ZWNJ);
267
268#[derive(Clone, Copy, PartialEq)]
269enum RephPosition {
270    AfterMain = ot_position_t::POS_AFTER_MAIN as isize,
271    BeforeSub = ot_position_t::POS_BEFORE_SUB as isize,
272    AfterSub = ot_position_t::POS_AFTER_SUB as isize,
273    BeforePost = ot_position_t::POS_BEFORE_POST as isize,
274    AfterPost = ot_position_t::POS_AFTER_POST as isize,
275}
276
277#[derive(Clone, Copy, PartialEq)]
278enum RephMode {
279    /// Reph formed out of initial Ra,H sequence.
280    Implicit,
281    /// Reph formed out of initial Ra,H,ZWJ sequence.
282    Explicit,
283    /// Encoded Repha character, needs reordering.
284    LogRepha,
285}
286
287#[derive(Clone, Copy, PartialEq)]
288enum BlwfMode {
289    /// Below-forms feature applied to pre-base and post-base.
290    PreAndPost,
291    /// Below-forms feature applied to post-base only.
292    PostOnly,
293}
294
295#[derive(Clone, Copy)]
296struct IndicConfig {
297    script: Option<Script>,
298    has_old_spec: bool,
299    virama: u32,
300    reph_pos: RephPosition,
301    reph_mode: RephMode,
302    blwf_mode: BlwfMode,
303}
304
305impl IndicConfig {
306    const fn new(
307        script: Option<Script>,
308        has_old_spec: bool,
309        virama: u32,
310        reph_pos: RephPosition,
311        reph_mode: RephMode,
312        blwf_mode: BlwfMode,
313    ) -> Self {
314        IndicConfig {
315            script,
316            has_old_spec,
317            virama,
318            reph_pos,
319            reph_mode,
320            blwf_mode,
321        }
322    }
323}
324
325static INDIC_CONFIGS: &[IndicConfig] = &[
326    IndicConfig::new(
327        None,
328        false,
329        0,
330        RephPosition::BeforePost,
331        RephMode::Implicit,
332        BlwfMode::PreAndPost,
333    ),
334    IndicConfig::new(
335        Some(script::DEVANAGARI),
336        true,
337        0x094D,
338        RephPosition::BeforePost,
339        RephMode::Implicit,
340        BlwfMode::PreAndPost,
341    ),
342    IndicConfig::new(
343        Some(script::BENGALI),
344        true,
345        0x09CD,
346        RephPosition::AfterSub,
347        RephMode::Implicit,
348        BlwfMode::PreAndPost,
349    ),
350    IndicConfig::new(
351        Some(script::GURMUKHI),
352        true,
353        0x0A4D,
354        RephPosition::BeforeSub,
355        RephMode::Implicit,
356        BlwfMode::PreAndPost,
357    ),
358    IndicConfig::new(
359        Some(script::GUJARATI),
360        true,
361        0x0ACD,
362        RephPosition::BeforePost,
363        RephMode::Implicit,
364        BlwfMode::PreAndPost,
365    ),
366    IndicConfig::new(
367        Some(script::ORIYA),
368        true,
369        0x0B4D,
370        RephPosition::AfterMain,
371        RephMode::Implicit,
372        BlwfMode::PreAndPost,
373    ),
374    IndicConfig::new(
375        Some(script::TAMIL),
376        true,
377        0x0BCD,
378        RephPosition::AfterPost,
379        RephMode::Implicit,
380        BlwfMode::PreAndPost,
381    ),
382    IndicConfig::new(
383        Some(script::TELUGU),
384        true,
385        0x0C4D,
386        RephPosition::AfterPost,
387        RephMode::Explicit,
388        BlwfMode::PostOnly,
389    ),
390    IndicConfig::new(
391        Some(script::KANNADA),
392        true,
393        0x0CCD,
394        RephPosition::AfterPost,
395        RephMode::Implicit,
396        BlwfMode::PostOnly,
397    ),
398    IndicConfig::new(
399        Some(script::MALAYALAM),
400        true,
401        0x0D4D,
402        RephPosition::AfterMain,
403        RephMode::LogRepha,
404        BlwfMode::PreAndPost,
405    ),
406    IndicConfig::new(
407        Some(script::SINHALA),
408        false,
409        0x0DCA,
410        RephPosition::AfterPost,
411        RephMode::Explicit,
412        BlwfMode::PreAndPost,
413    ),
414];
415
416struct IndicWouldSubstituteFeature {
417    lookups: Range<usize>,
418    zero_context: bool,
419}
420
421impl IndicWouldSubstituteFeature {
422    pub fn new(map: &hb_ot_map_t, feature_tag: hb_tag_t, zero_context: bool) -> Self {
423        IndicWouldSubstituteFeature {
424            lookups: match map.get_feature_stage(TableIndex::GSUB, feature_tag) {
425                Some(stage) => map.stage_lookup_range(TableIndex::GSUB, stage),
426                None => 0..0,
427            },
428            zero_context,
429        }
430    }
431
432    pub fn would_substitute(
433        &self,
434        map: &hb_ot_map_t,
435        face: &hb_font_t,
436        glyphs: &[GlyphId],
437    ) -> bool {
438        for index in self.lookups.clone() {
439            let lookup = map.lookup(TableIndex::GSUB, index);
440            let ctx = WouldApplyContext {
441                glyphs,
442                zero_context: self.zero_context,
443            };
444            if face
445                .ot_tables
446                .gsub
447                .as_ref()
448                .and_then(|table| table.get_lookup(lookup.index))
449                .is_some_and(|lookup| lookup.would_apply(face, &ctx) == Some(true))
450            {
451                return true;
452            }
453        }
454
455        false
456    }
457}
458
459struct IndicShapePlan {
460    config: IndicConfig,
461    is_old_spec: bool,
462    // virama_glyph: Option<u32>,
463    rphf: IndicWouldSubstituteFeature,
464    pref: IndicWouldSubstituteFeature,
465    blwf: IndicWouldSubstituteFeature,
466    pstf: IndicWouldSubstituteFeature,
467    vatu: IndicWouldSubstituteFeature,
468    mask_array: [hb_mask_t; INDIC_FEATURES.len()],
469}
470
471impl IndicShapePlan {
472    fn new(plan: &hb_ot_shape_plan_t) -> Self {
473        let script = plan.script;
474        let config = if let Some(c) = INDIC_CONFIGS.iter().skip(1).find(|c| c.script == script) {
475            *c
476        } else {
477            INDIC_CONFIGS[0]
478        };
479
480        let is_old_spec = config.has_old_spec
481            && plan
482                .ot_map
483                .chosen_script(TableIndex::GSUB)
484                .is_none_or(|tag| tag.to_be_bytes()[3] != b'2');
485
486        // Use zero-context would_substitute() matching for new-spec of the main
487        // Indic scripts, and scripts with one spec only, but not for old-specs.
488        // The new-spec for all dual-spec scripts says zero-context matching happens.
489        //
490        // However, testing with Malayalam shows that old and new spec both allow
491        // context.  Testing with Bengali new-spec however shows that it doesn't.
492        // So, the heuristic here is the way it is.  It should *only* be changed,
493        // as we discover more cases of what Windows does.  DON'T TOUCH OTHERWISE.
494        let zero_context = is_old_spec && script != Some(script::MALAYALAM);
495
496        let mut mask_array = [0; INDIC_FEATURES.len()];
497        for (i, feature) in INDIC_FEATURES.iter().enumerate() {
498            mask_array[i] = if feature.1 & F_GLOBAL != 0 {
499                0
500            } else {
501                plan.ot_map.get_1_mask(feature.0)
502            }
503        }
504
505        // TODO: what is this?
506        // let mut virama_glyph = None;
507        // if config.virama != 0 {
508        //     if let Some(g) = face.glyph_index(char::try_from(config.virama).unwrap()) {
509        //         virama_glyph = Some(g.0 as u32);
510        //     }
511        // }
512
513        IndicShapePlan {
514            config,
515            is_old_spec,
516            // virama_glyph,
517            rphf: IndicWouldSubstituteFeature::new(
518                &plan.ot_map,
519                hb_tag_t::new(b"rphf"),
520                zero_context,
521            ),
522            pref: IndicWouldSubstituteFeature::new(
523                &plan.ot_map,
524                hb_tag_t::new(b"pref"),
525                zero_context,
526            ),
527            blwf: IndicWouldSubstituteFeature::new(
528                &plan.ot_map,
529                hb_tag_t::new(b"blwf"),
530                zero_context,
531            ),
532            pstf: IndicWouldSubstituteFeature::new(
533                &plan.ot_map,
534                hb_tag_t::new(b"pstf"),
535                zero_context,
536            ),
537            vatu: IndicWouldSubstituteFeature::new(
538                &plan.ot_map,
539                hb_tag_t::new(b"vatu"),
540                zero_context,
541            ),
542            mask_array,
543        }
544    }
545}
546
547fn collect_features(planner: &mut hb_ot_shape_planner_t) {
548    // Do this before any lookups have been applied.
549    planner.ot_map.add_gsub_pause(Some(setup_syllables));
550
551    planner
552        .ot_map
553        .enable_feature(hb_tag_t::new(b"locl"), F_PER_SYLLABLE, 1);
554    // The Indic specs do not require ccmp, but we apply it here since if
555    // there is a use of it, it's typically at the beginning.
556    planner
557        .ot_map
558        .enable_feature(hb_tag_t::new(b"ccmp"), F_PER_SYLLABLE, 1);
559
560    planner.ot_map.add_gsub_pause(Some(initial_reordering));
561
562    for feature in INDIC_FEATURES.iter().take(11) {
563        planner.ot_map.add_feature(feature.0, feature.1, 1);
564        planner.ot_map.add_gsub_pause(None);
565    }
566
567    planner.ot_map.add_gsub_pause(Some(final_reordering));
568
569    for feature in INDIC_FEATURES.iter().skip(11) {
570        planner.ot_map.add_feature(feature.0, feature.1, 1);
571    }
572}
573
574fn override_features(planner: &mut hb_ot_shape_planner_t) {
575    planner.ot_map.disable_feature(hb_tag_t::new(b"liga"));
576    planner.ot_map.add_gsub_pause(Some(syllabic_clear_var)); // Don't need syllables anymore.
577}
578
579fn preprocess_text(_: &hb_ot_shape_plan_t, _: &mut FontFuncsDispatch, buffer: &mut hb_buffer_t) {
580    super::ot_shaper_vowel_constraints::preprocess_text_vowel_constraints(buffer);
581}
582
583fn decompose(_: &hb_ot_shape_normalize_context_t, ab: Codepoint) -> Option<(Codepoint, Codepoint)> {
584    // Don't decompose these.
585    match ab {
586        0x0931 |               // DEVANAGARI LETTER RRA
587        // https://github.com/harfbuzz/harfbuzz/issues/779
588        0x09DC |               // BENGALI LETTER RRA
589        0x09DD |               // BENGALI LETTER RHA
590        0x0B94 => return None, // TAMIL LETTER AU
591        _ => {}
592    }
593
594    crate::hb::unicode::decompose(ab)
595}
596
597fn compose(_: &hb_ot_shape_normalize_context_t, a: Codepoint, b: Codepoint) -> Option<Codepoint> {
598    // Avoid recomposing split matras.
599    if a.general_category().is_mark() {
600        return None;
601    }
602
603    // Composition-exclusion exceptions that we want to recompose.
604    if a == 0x09AF && b == 0x09BC {
605        return Some(0x09DF);
606    }
607
608    crate::hb::unicode::compose(a, b)
609}
610
611fn setup_masks(_: &hb_ot_shape_plan_t, _: &mut FontFuncsDispatch, buffer: &mut hb_buffer_t) {
612    buffer.allocate_var(GlyphInfo::INDIC_CATEGORY_VAR);
613    buffer.allocate_var(GlyphInfo::INDIC_POSITION_VAR);
614
615    // We cannot setup masks here.  We save information about characters
616    // and setup masks later on in a pause-callback.
617    for info in buffer.info_slice_mut() {
618        info.set_indic_properties();
619    }
620}
621
622fn setup_syllables(
623    _: &hb_ot_shape_plan_t,
624    _: &mut FontFuncsDispatch,
625    buffer: &mut hb_buffer_t,
626) -> bool {
627    buffer.allocate_var(GlyphInfo::SYLLABLE_VAR);
628
629    super::ot_shaper_indic_machine::find_syllables_indic(buffer);
630
631    let mut start = 0;
632    let mut end = buffer.next_syllable(0);
633    while start < buffer.len {
634        buffer.unsafe_to_break(Some(start), Some(end));
635        start = end;
636        end = buffer.next_syllable(start);
637    }
638
639    false
640}
641
642fn initial_reordering(
643    plan: &hb_ot_shape_plan_t,
644    font_funcs: &mut FontFuncsDispatch,
645    buffer: &mut hb_buffer_t,
646) -> bool {
647    use super::ot_shaper_indic_machine::SyllableType;
648
649    let mut ret = false;
650
651    let indic_plan = plan.data::<IndicShapePlan>();
652
653    update_consonant_positions(plan, indic_plan, font_funcs, buffer);
654    if insert_dotted_circles(
655        font_funcs,
656        buffer,
657        SyllableType::BrokenCluster as u8,
658        ot_category_t::OT_DOTTEDCIRCLE,
659        Some(ot_category_t::OT_Repha),
660        Some(ot_position_t::POS_END),
661    ) {
662        ret = true;
663    }
664
665    let mut start = 0;
666    let mut end = buffer.next_syllable(0);
667    while start < buffer.len {
668        initial_reordering_syllable(plan, indic_plan, font_funcs, start, end, buffer);
669        start = end;
670        end = buffer.next_syllable(start);
671    }
672
673    ret
674}
675
676fn update_consonant_positions(
677    plan: &hb_ot_shape_plan_t,
678    indic_plan: &IndicShapePlan,
679    font_funcs: &mut FontFuncsDispatch,
680    buffer: &mut hb_buffer_t,
681) {
682    let mut virama_glyph = None;
683    if indic_plan.config.virama != 0 {
684        virama_glyph = font_funcs.nominal_glyph(indic_plan.config.virama);
685    }
686
687    if let Some(virama) = virama_glyph {
688        let face = font_funcs.font();
689        for info in buffer.info_slice_mut() {
690            if info.indic_position() == ot_position_t::POS_BASE_C {
691                let consonant = info.as_glyph();
692                info.set_indic_position(consonant_position_from_face(
693                    plan, indic_plan, face, consonant, virama,
694                ));
695            }
696        }
697    }
698}
699
700fn consonant_position_from_face(
701    plan: &hb_ot_shape_plan_t,
702    indic_plan: &IndicShapePlan,
703    face: &hb_font_t,
704    consonant: GlyphId,
705    virama: GlyphId,
706) -> u8 {
707    // For old-spec, the order of glyphs is Consonant,Virama,
708    // whereas for new-spec, it's Virama,Consonant.  However,
709    // some broken fonts (like Free Sans) simply copied lookups
710    // from old-spec to new-spec without modification.
711    // And oddly enough, Uniscribe seems to respect those lookups.
712    // Eg. in the sequence U+0924,U+094D,U+0930, Uniscribe finds
713    // base at 0.  The font however, only has lookups matching
714    // 930,94D in 'blwf', not the expected 94D,930 (with new-spec
715    // table).  As such, we simply match both sequences.  Seems
716    // to work.
717    //
718    // Vatu is done as well, for:
719    // https://github.com/harfbuzz/harfbuzz/issues/1587
720
721    if indic_plan
722        .blwf
723        .would_substitute(&plan.ot_map, face, &[virama, consonant])
724        || indic_plan
725            .blwf
726            .would_substitute(&plan.ot_map, face, &[consonant, virama])
727        || indic_plan
728            .vatu
729            .would_substitute(&plan.ot_map, face, &[virama, consonant])
730        || indic_plan
731            .vatu
732            .would_substitute(&plan.ot_map, face, &[consonant, virama])
733    {
734        return ot_position_t::POS_BELOW_C;
735    }
736
737    if indic_plan
738        .pstf
739        .would_substitute(&plan.ot_map, face, &[virama, consonant])
740        || indic_plan
741            .pstf
742            .would_substitute(&plan.ot_map, face, &[consonant, virama])
743    {
744        return ot_position_t::POS_POST_C;
745    }
746
747    if indic_plan
748        .pref
749        .would_substitute(&plan.ot_map, face, &[virama, consonant])
750        || indic_plan
751            .pref
752            .would_substitute(&plan.ot_map, face, &[consonant, virama])
753    {
754        return ot_position_t::POS_POST_C;
755    }
756
757    ot_position_t::POS_BASE_C
758}
759
760fn initial_reordering_syllable(
761    plan: &hb_ot_shape_plan_t,
762    indic_plan: &IndicShapePlan,
763    font_funcs: &mut FontFuncsDispatch,
764    start: usize,
765    end: usize,
766    buffer: &mut hb_buffer_t,
767) {
768    use super::ot_shaper_indic_machine::SyllableType;
769
770    let syllable_type = match buffer.info[start].syllable() & 0x0F {
771        0 => SyllableType::ConsonantSyllable,
772        1 => SyllableType::VowelSyllable,
773        2 => SyllableType::StandaloneCluster,
774        3 => SyllableType::SymbolCluster,
775        4 => SyllableType::BrokenCluster,
776        5 => SyllableType::NonIndicCluster,
777        _ => unreachable!(),
778    };
779
780    match syllable_type {
781        // We made the vowels look like consonants.  So let's call the consonant logic!
782        SyllableType::VowelSyllable | SyllableType::ConsonantSyllable => {
783            initial_reordering_consonant_syllable(plan, indic_plan, font_funcs, start, end, buffer);
784        }
785        // We already inserted dotted-circles, so just call the standalone_cluster.
786        SyllableType::BrokenCluster | SyllableType::StandaloneCluster => {
787            initial_reordering_standalone_cluster(plan, indic_plan, font_funcs, start, end, buffer);
788        }
789        SyllableType::SymbolCluster | SyllableType::NonIndicCluster => {}
790    }
791}
792
793// Rules from:
794// https://docs.microsqoft.com/en-us/typography/script-development/devanagari */
795fn initial_reordering_consonant_syllable(
796    plan: &hb_ot_shape_plan_t,
797    indic_plan: &IndicShapePlan,
798    font_funcs: &mut FontFuncsDispatch,
799    start: usize,
800    end: usize,
801    buffer: &mut hb_buffer_t,
802) {
803    let face = font_funcs.font();
804    // https://github.com/harfbuzz/harfbuzz/issues/435#issuecomment-335560167
805    // For compatibility with legacy usage in Kannada,
806    // Ra+h+ZWJ must behave like Ra+ZWJ+h...
807    if buffer.script == Some(script::KANNADA)
808        && start + 3 <= end
809        && buffer.info[start].is_one_of(category_flag(ot_category_t::OT_Ra))
810        && buffer.info[start + 1].is_one_of(category_flag(ot_category_t::OT_H))
811        && buffer.info[start + 2].is_one_of(category_flag(ot_category_t::OT_ZWJ))
812    {
813        buffer.merge_clusters(start + 1, start + 3);
814        buffer.info.swap(start + 1, start + 2);
815    }
816
817    // 1. Find base consonant:
818    //
819    // The shaping engine finds the base consonant of the syllable, using the
820    // following algorithm: starting from the end of the syllable, move backwards
821    // until a consonant is found that does not have a below-base or post-base
822    // form (post-base forms have to follow below-base forms), or that is not a
823    // pre-base-reordering Ra, or arrive at the first consonant. The consonant
824    // stopped at will be the base.
825    //
826    //   - If the syllable starts with Ra + Halant (in a script that has Reph)
827    //     and has more than one consonant, Ra is excluded from candidates for
828    //     base consonants.
829
830    let mut base = end;
831    let mut has_reph = false;
832
833    {
834        // -> If the syllable starts with Ra + Halant (in a script that has Reph)
835        //    and has more than one consonant, Ra is excluded from candidates for
836        //    base consonants.
837        let mut limit = start;
838        if indic_plan.mask_array[indic_feature::RPHF] != 0
839            && start + 3 <= end
840            && ((indic_plan.config.reph_mode == RephMode::Implicit
841                && !buffer.info[start + 2].is_joiner())
842                || (indic_plan.config.reph_mode == RephMode::Explicit
843                    && buffer.info[start + 2].indic_category() == ot_category_t::OT_ZWJ))
844        {
845            // See if it matches the 'rphf' feature.
846            let glyphs = &[
847                buffer.info[start].as_glyph(),
848                buffer.info[start + 1].as_glyph(),
849                if indic_plan.config.reph_mode == RephMode::Explicit {
850                    buffer.info[start + 2].as_glyph()
851                } else {
852                    GlyphId::NOTDEF
853                },
854            ];
855            if indic_plan
856                .rphf
857                .would_substitute(&plan.ot_map, face, &glyphs[0..2])
858                || (indic_plan.config.reph_mode == RephMode::Explicit
859                    && indic_plan.rphf.would_substitute(&plan.ot_map, face, glyphs))
860            {
861                limit += 2;
862                while limit < end && buffer.info[limit].is_joiner() {
863                    limit += 1;
864                }
865                base = start;
866                has_reph = true;
867            }
868        } else if indic_plan.config.reph_mode == RephMode::LogRepha
869            && buffer.info[start].indic_category() == ot_category_t::OT_Repha
870        {
871            limit += 1;
872            while limit < end && buffer.info[limit].is_joiner() {
873                limit += 1;
874            }
875            base = start;
876            has_reph = true;
877        }
878
879        {
880            // -> starting from the end of the syllable, move backwards
881            let mut i = end;
882            let mut seen_below = false;
883            loop {
884                i -= 1;
885                // -> until a consonant is found
886                if buffer.info[i].is_consonant() {
887                    // -> that does not have a below-base or post-base form
888                    // (post-base forms have to follow below-base forms),
889                    if buffer.info[i].indic_position() != ot_position_t::POS_BELOW_C
890                        && (buffer.info[i].indic_position() != ot_position_t::POS_POST_C
891                            || seen_below)
892                    {
893                        base = i;
894                        break;
895                    }
896                    if buffer.info[i].indic_position() == ot_position_t::POS_BELOW_C {
897                        seen_below = true;
898                    }
899
900                    // -> or that is not a pre-base-reordering Ra,
901                    //
902                    // IMPLEMENTATION NOTES:
903                    //
904                    // Our pre-base-reordering Ra's are marked position::PostC, so will be skipped
905                    // by the logic above already.
906
907                    // -> or arrive at the first consonant. The consonant stopped at will
908                    // be the base.
909                    base = i;
910                } else {
911                    // A ZWJ after a Halant stops the base search, and requests an explicit
912                    // half form.
913                    // A ZWJ before a Halant, requests a subjoined form instead, and hence
914                    // search continues.  This is particularly important for Bengali
915                    // sequence Ra,H,Ya that should form Ya-Phalaa by subjoining Ya.
916                    if start < i
917                        && buffer.info[i].indic_category() == ot_category_t::OT_ZWJ
918                        && buffer.info[i - 1].indic_category() == ot_category_t::OT_H
919                    {
920                        break;
921                    }
922                }
923
924                if i <= limit {
925                    break;
926                }
927            }
928        }
929
930        // -> If the syllable starts with Ra + Halant (in a script that has Reph)
931        //    and has more than one consonant, Ra is excluded from candidates for
932        //    base consonants.
933        //
934        //  Only do this for unforced Reph. (ie. not for Ra,H,ZWJ.
935        if has_reph && base == start && limit - base <= 2 {
936            // Have no other consonant, so Reph is not formed and Ra becomes base.
937            has_reph = false;
938        }
939    }
940
941    // 2. Decompose and reorder Matras:
942    //
943    // Each matra and any syllable modifier sign in the syllable are moved to the
944    // appropriate position relative to the consonant(s) in the syllable. The
945    // shaping engine decomposes two- or three-part matras into their constituent
946    // parts before any repositioning. Matra characters are classified by which
947    // consonant in a conjunct they have affinity for and are reordered to the
948    // following positions:
949    //
950    //   - Before first half form in the syllable
951    //   - After subjoined consonants
952    //   - After post-form consonant
953    //   - After main consonant (for above marks)
954    //
955    // IMPLEMENTATION NOTES:
956    //
957    // The normalize() routine has already decomposed matras for us, so we don't
958    // need to worry about that.
959
960    // 3.  Reorder marks to canonical order:
961    //
962    // Adjacent nukta and halant or nukta and vedic sign are always repositioned
963    // if necessary, so that the nukta is first.
964    //
965    // IMPLEMENTATION NOTES:
966    //
967    // We don't need to do this: the normalize() routine already did this for us.
968
969    // Reorder characters
970
971    for i in start..base {
972        let pos = buffer.info[i].indic_position();
973        buffer.info[i].set_indic_position(cmp::min(ot_position_t::POS_PRE_C, pos));
974    }
975
976    if base < end {
977        buffer.info[base].set_indic_position(ot_position_t::POS_BASE_C);
978    }
979
980    // Handle beginning Ra
981    if has_reph {
982        buffer.info[start].set_indic_position(ot_position_t::POS_RA_TO_BECOME_REPH);
983    }
984
985    // For old-style Indic script tags, move the first post-base Halant after
986    // last consonant.
987    //
988    // Reports suggest that in some scripts Uniscribe does this only if there
989    // is *not* a Halant after last consonant already.  We know that is the
990    // case for Kannada, while it reorders unconditionally in other scripts,
991    // eg. Malayalam, Bengali, and Devanagari.  We don't currently know about
992    // other scripts, so we block Kannada.
993    //
994    // Kannada test case:
995    // U+0C9A,U+0CCD,U+0C9A,U+0CCD
996    // With some versions of Lohit Kannada.
997    // https://bugs.freedesktop.org/show_bug.cgi?id=59118
998    //
999    // Malayalam test case:
1000    // U+0D38,U+0D4D,U+0D31,U+0D4D,U+0D31,U+0D4D
1001    // With lohit-ttf-20121122/Lohit-Malayalam.ttf
1002    //
1003    // Bengali test case:
1004    // U+0998,U+09CD,U+09AF,U+09CD
1005    // With Windows XP vrinda.ttf
1006    // https://github.com/harfbuzz/harfbuzz/issues/1073
1007    //
1008    // Devanagari test case:
1009    // U+091F,U+094D,U+0930,U+094D
1010    // With chandas.ttf
1011    // https://github.com/harfbuzz/harfbuzz/issues/1071
1012    if indic_plan.is_old_spec {
1013        let disallow_double_halants = buffer.script == Some(script::KANNADA);
1014        for i in base + 1..end {
1015            if buffer.info[i].indic_category() == ot_category_t::OT_H {
1016                let mut j = end - 1;
1017                while j > i {
1018                    if buffer.info[j].is_consonant()
1019                        || (disallow_double_halants
1020                            && buffer.info[j].indic_category() == ot_category_t::OT_H)
1021                    {
1022                        break;
1023                    }
1024
1025                    j -= 1;
1026                }
1027
1028                if buffer.info[j].indic_category() != ot_category_t::OT_H && j > i {
1029                    // Move Halant to after last consonant.
1030                    let t = buffer.info[i];
1031                    for k in 0..j - i {
1032                        buffer.info[k + i] = buffer.info[k + i + 1];
1033                    }
1034                    buffer.info[j] = t;
1035                }
1036
1037                break;
1038            }
1039        }
1040    }
1041
1042    // Attach misc marks to previous char to move with them.
1043    {
1044        let mut last_pos = ot_position_t::POS_START;
1045        for i in start..end {
1046            let ok = rb_flag_unsafe(buffer.info[i].indic_category() as u32)
1047                & (category_flag(ot_category_t::OT_ZWJ)
1048                    | category_flag(ot_category_t::OT_ZWNJ)
1049                    | category_flag(ot_category_t::OT_N)
1050                    | category_flag(ot_category_t::OT_RS)
1051                    | category_flag(ot_category_t::OT_CM)
1052                    | category_flag(ot_category_t::OT_H))
1053                != 0;
1054            if ok {
1055                buffer.info[i].set_indic_position(last_pos);
1056
1057                if buffer.info[i].indic_category() == ot_category_t::OT_H
1058                    && buffer.info[i].indic_position() == ot_position_t::POS_PRE_M
1059                {
1060                    // Uniscribe doesn't move the Halant with Left Matra.
1061                    // TEST: U+092B,U+093F,U+094DE
1062                    // We follow.
1063                    for j in (start + 1..=i).rev() {
1064                        if buffer.info[j - 1].indic_position() != ot_position_t::POS_PRE_M {
1065                            let pos = buffer.info[j - 1].indic_position();
1066                            buffer.info[i].set_indic_position(pos);
1067                            break;
1068                        }
1069                    }
1070                }
1071            } else if buffer.info[i].indic_position() != ot_position_t::POS_SMVD {
1072                if buffer.info[i].indic_category() == ot_category_t::OT_MPst
1073                    && i > start
1074                    && buffer.info[i - 1].indic_category() == ot_category_t::OT_SM
1075                {
1076                    let val = buffer.info[i].indic_position();
1077                    buffer.info[i - 1].set_indic_position(val);
1078                }
1079
1080                last_pos = buffer.info[i].indic_position();
1081            }
1082        }
1083    }
1084    // For post-base consonants let them own anything before them
1085    // since the last consonant or matra.
1086    {
1087        let mut last = base;
1088        for i in base + 1..end {
1089            if buffer.info[i].is_consonant() {
1090                for j in last + 1..i {
1091                    if buffer.info[j].indic_position() < ot_position_t::POS_SMVD {
1092                        let pos = buffer.info[i].indic_position();
1093                        buffer.info[j].set_indic_position(pos);
1094                    }
1095                }
1096
1097                last = i;
1098            } else if (rb_flag_unsafe(buffer.info[i].indic_category() as u32)
1099                & (rb_flag(ot_category_t::OT_M as u32) | rb_flag(ot_category_t::OT_MPst as u32)))
1100                != 0
1101            {
1102                last = i;
1103            }
1104        }
1105    }
1106
1107    {
1108        // Use syllable() for sort accounting temporarily.
1109        let syllable = buffer.info[start].syllable();
1110        for i in start..end {
1111            // We don't care about overflow here as we won't actually use these
1112            // values if `end - start > 127`.
1113            buffer.info[i].set_syllable((i - start) as u8);
1114        }
1115
1116        buffer.info[start..end].sort_by_key(|a| a.indic_position());
1117
1118        // Find base again; also flip left-matra sequence.
1119        let mut first_left_mantra = end;
1120        let mut last_left_mantra = end;
1121        base = end;
1122
1123        for i in start..end {
1124            if buffer.info[i].indic_position() == ot_position_t::POS_BASE_C {
1125                base = i;
1126                break;
1127            } else if buffer.info[i].indic_position() == ot_position_t::POS_PRE_M {
1128                if first_left_mantra == end {
1129                    first_left_mantra = i;
1130                }
1131
1132                last_left_mantra = i;
1133            }
1134        }
1135
1136        // https://github.com/harfbuzz/harfbuzz/issues/3863
1137        if first_left_mantra < last_left_mantra {
1138            // No need to merge clusters, handled later.
1139            buffer.reverse_range(first_left_mantra, last_left_mantra + 1);
1140            // Reverse back nuktas, etc.
1141            let mut i = first_left_mantra;
1142
1143            for j in i..=last_left_mantra {
1144                if (rb_flag_unsafe(buffer.info[j].indic_category() as u32)
1145                    & (rb_flag(ot_category_t::OT_M as u32)
1146                        | rb_flag(ot_category_t::OT_MPst as u32)))
1147                    != 0
1148                {
1149                    buffer.reverse_range(i, j + 1);
1150                    i = j + 1;
1151                }
1152            }
1153        }
1154
1155        // Things are out-of-control for post base positions, they may shuffle
1156        // around like crazy.  In old-spec mode, we move halants around, so in
1157        // that case merge all clusters after base.  Otherwise, check the sort
1158        // order and merge as needed.
1159        // For pre-base stuff, we handle cluster issues in final reordering.
1160        //
1161        // We could use buffer->sort() for this, if there was no special
1162        // reordering of pre-base stuff happening later...
1163        // We don't want to merge_clusters all of that, which buffer->sort()
1164        // would.  Here's a concrete example:
1165        //
1166        // Assume there's a pre-base consonant and explicit Halant before base,
1167        // followed by a prebase-reordering (left) Matra:
1168        //
1169        //   C,H,ZWNJ,B,M
1170        //
1171        // At this point in reordering we would have:
1172        //
1173        //   M,C,H,ZWNJ,B
1174        //
1175        // whereas in final reordering we will bring the Matra closer to Base:
1176        //
1177        //   C,H,ZWNJ,M,B
1178        //
1179        // That's why we don't want to merge-clusters anything before the Base
1180        // at this point.  But if something moved from after Base to before it,
1181        // we should merge clusters from base to them.  In final-reordering, we
1182        // only move things around before base, and merge-clusters up to base.
1183        // These two merge-clusters from the two sides of base will interlock
1184        // to merge things correctly.  See:
1185        // https://github.com/harfbuzz/harfbuzz/issues/2272
1186        if indic_plan.is_old_spec || end - start > 127 {
1187            buffer.merge_clusters(base, end);
1188        } else {
1189            // Note! syllable() is a one-byte field.
1190            for i in base..end {
1191                if buffer.info[i].syllable() != 255 {
1192                    let mut min = i;
1193                    let mut max = i;
1194                    let mut j = start + buffer.info[i].syllable() as usize;
1195                    while j != i {
1196                        min = cmp::min(min, j);
1197                        max = cmp::max(max, j);
1198                        let next = start + buffer.info[j].syllable() as usize;
1199                        buffer.info[j].set_syllable(255); // So we don't process j later again.
1200                        j = next;
1201                    }
1202
1203                    buffer.merge_clusters(cmp::max(base, min), max + 1);
1204                }
1205            }
1206        }
1207
1208        // Put syllable back in.
1209        for info in &mut buffer.info[start..end] {
1210            info.set_syllable(syllable);
1211        }
1212    }
1213
1214    // Setup masks now
1215
1216    {
1217        // Reph
1218        for info in &mut buffer.info[start..end] {
1219            if info.indic_position() != ot_position_t::POS_RA_TO_BECOME_REPH {
1220                break;
1221            }
1222
1223            info.mask |= indic_plan.mask_array[indic_feature::RPHF];
1224        }
1225
1226        // Pre-base
1227        let mut mask = indic_plan.mask_array[indic_feature::HALF];
1228        if !indic_plan.is_old_spec && indic_plan.config.blwf_mode == BlwfMode::PreAndPost {
1229            mask |= indic_plan.mask_array[indic_feature::BLWF];
1230        }
1231
1232        for info in &mut buffer.info[start..base] {
1233            info.mask |= mask;
1234        }
1235
1236        // Base
1237        mask = 0;
1238        if base < end {
1239            buffer.info[base].mask |= mask;
1240        }
1241
1242        // Post-base
1243        mask = indic_plan.mask_array[indic_feature::BLWF]
1244            | indic_plan.mask_array[indic_feature::ABVF]
1245            | indic_plan.mask_array[indic_feature::PSTF];
1246        for i in base + 1..end {
1247            buffer.info[i].mask |= mask;
1248        }
1249    }
1250
1251    if indic_plan.is_old_spec && buffer.script == Some(script::DEVANAGARI) {
1252        // Old-spec eye-lash Ra needs special handling.  From the
1253        // spec:
1254        //
1255        // "The feature 'below-base form' is applied to consonants
1256        // having below-base forms and following the base consonant.
1257        // The exception is vattu, which may appear below half forms
1258        // as well as below the base glyph. The feature 'below-base
1259        // form' will be applied to all such occurrences of Ra as well."
1260        //
1261        // Test case: U+0924,U+094D,U+0930,U+094d,U+0915
1262        // with Sanskrit 2003 font.
1263        //
1264        // However, note that Ra,Halant,ZWJ is the correct way to
1265        // request eyelash form of Ra, so we wouldbn't inhibit it
1266        // in that sequence.
1267        //
1268        // Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915
1269        for i in start..base.saturating_sub(1) {
1270            if buffer.info[i].indic_category() == ot_category_t::OT_Ra
1271                && buffer.info[i + 1].indic_category() == ot_category_t::OT_H
1272                && (i + 2 == base || buffer.info[i + 2].indic_category() != ot_category_t::OT_ZWJ)
1273            {
1274                buffer.info[i].mask |= indic_plan.mask_array[indic_feature::BLWF];
1275                buffer.info[i + 1].mask |= indic_plan.mask_array[indic_feature::BLWF];
1276            }
1277        }
1278    }
1279
1280    let pref_len = 2;
1281    if indic_plan.mask_array[indic_feature::PREF] != 0 && base + pref_len < end {
1282        // Find a Halant,Ra sequence and mark it for pre-base-reordering processing.
1283        for i in base + 1..end - pref_len + 1 {
1284            let glyphs = &[buffer.info[i + 0].as_glyph(), buffer.info[i + 1].as_glyph()];
1285            if indic_plan.pref.would_substitute(&plan.ot_map, face, glyphs) {
1286                buffer.info[i + 0].mask |= indic_plan.mask_array[indic_feature::PREF];
1287                buffer.info[i + 1].mask |= indic_plan.mask_array[indic_feature::PREF];
1288                break;
1289            }
1290        }
1291    }
1292
1293    // Apply ZWJ/ZWNJ effects
1294    for i in start + 1..end {
1295        if buffer.info[i].is_joiner() {
1296            let non_joiner = buffer.info[i].indic_category() == ot_category_t::OT_ZWNJ;
1297            let mut j = i;
1298
1299            loop {
1300                j -= 1;
1301
1302                // ZWJ/ZWNJ should disable CJCT.  They do that by simply
1303                // being there, since we don't skip them for the CJCT
1304                // feature (ie. F_MANUAL_ZWJ)
1305
1306                // A ZWNJ disables HALF.
1307                if non_joiner {
1308                    buffer.info[j].mask &= !indic_plan.mask_array[indic_feature::HALF];
1309                }
1310
1311                if j <= start || buffer.info[j].is_consonant() {
1312                    break;
1313                }
1314            }
1315        }
1316    }
1317}
1318
1319fn initial_reordering_standalone_cluster(
1320    plan: &hb_ot_shape_plan_t,
1321    indic_plan: &IndicShapePlan,
1322    face: &mut FontFuncsDispatch,
1323    start: usize,
1324    end: usize,
1325    buffer: &mut hb_buffer_t,
1326) {
1327    // We treat placeholder/dotted-circle as if they are consonants, so we
1328    // should just chain.  Only if not in compatibility mode that is...
1329    initial_reordering_consonant_syllable(plan, indic_plan, face, start, end, buffer);
1330}
1331
1332fn final_reordering(
1333    plan: &hb_ot_shape_plan_t,
1334    face: &mut FontFuncsDispatch,
1335    buffer: &mut hb_buffer_t,
1336) -> bool {
1337    if buffer.is_empty() {
1338        return false;
1339    }
1340
1341    foreach_syllable!(buffer, start, end, {
1342        final_reordering_impl(plan, face, start, end, buffer);
1343    });
1344
1345    buffer.deallocate_var(GlyphInfo::INDIC_CATEGORY_VAR);
1346    buffer.deallocate_var(GlyphInfo::INDIC_POSITION_VAR);
1347
1348    false
1349}
1350
1351fn final_reordering_impl(
1352    plan: &hb_ot_shape_plan_t,
1353    face: &mut FontFuncsDispatch,
1354    start: usize,
1355    end: usize,
1356    buffer: &mut hb_buffer_t,
1357) {
1358    let indic_plan = plan.data::<IndicShapePlan>();
1359
1360    // This function relies heavily on halant glyphs.  Lots of ligation
1361    // and possibly multiple substitutions happened prior to this
1362    // phase, and that might have messed up our properties.  Recover
1363    // from a particular case of that where we're fairly sure that a
1364    // class of OT_H is desired but has been lost.
1365    //
1366    // We don't call load_virama_glyph(), since we know it's already loaded.
1367    let mut virama_glyph = None;
1368    if indic_plan.config.virama != 0 {
1369        if let Some(g) = face.nominal_glyph(indic_plan.config.virama) {
1370            virama_glyph = Some(g.to_u32());
1371        }
1372    }
1373
1374    if let Some(virama_glyph) = virama_glyph {
1375        for info in &mut buffer.info[start..end] {
1376            if info.glyph_id == virama_glyph && info.ligated() && info.multiplied() {
1377                // This will make sure that this glyph passes is_halant() test.
1378                info.set_indic_category(ot_category_t::OT_H);
1379                info.clear_ligated_and_multiplied();
1380            }
1381        }
1382    }
1383
1384    // 4. Final reordering:
1385    //
1386    // After the localized forms and basic shaping forms GSUB features have been
1387    // applied (see below), the shaping engine performs some final glyph
1388    // reordering before applying all the remaining font features to the entire
1389    // syllable.
1390
1391    let mut try_pref = indic_plan.mask_array[indic_feature::PREF] != 0;
1392
1393    let mut base = start;
1394    while base < end {
1395        if buffer.info[base].indic_position() as u32 >= ot_position_t::POS_BASE_C as u32 {
1396            if try_pref && base + 1 < end {
1397                for i in base + 1..end {
1398                    if (buffer.info[i].mask & indic_plan.mask_array[indic_feature::PREF]) != 0 {
1399                        if !(buffer.info[i].substituted()
1400                            && buffer.info[i].ligated_and_didnt_multiply())
1401                        {
1402                            // Ok, this was a 'pref' candidate but didn't form any.
1403                            // Base is around here...
1404                            base = i;
1405                            while base < end && buffer.info[base].is_halant() {
1406                                base += 1;
1407                            }
1408
1409                            if base < end {
1410                                buffer.info[base].set_indic_position(ot_position_t::POS_BASE_C);
1411                            }
1412
1413                            try_pref = false;
1414                        }
1415
1416                        break;
1417                    }
1418
1419                    if base == end {
1420                        break;
1421                    }
1422                }
1423            }
1424
1425            // For Malayalam, skip over unformed below- (but NOT post-) forms.
1426            if buffer.script == Some(script::MALAYALAM) {
1427                let mut i = base + 1;
1428                while i < end {
1429                    while i < end && buffer.info[i].is_joiner() {
1430                        i += 1;
1431                    }
1432
1433                    if i == end || !buffer.info[i].is_halant() {
1434                        break;
1435                    }
1436
1437                    i += 1; // Skip halant.
1438
1439                    while i < end && buffer.info[i].is_joiner() {
1440                        i += 1;
1441                    }
1442
1443                    if i < end
1444                        && buffer.info[i].is_consonant()
1445                        && buffer.info[i].indic_position() == ot_position_t::POS_BELOW_C
1446                    {
1447                        base = i;
1448                        buffer.info[base].set_indic_position(ot_position_t::POS_BASE_C);
1449                    }
1450
1451                    i += 1;
1452                }
1453            }
1454
1455            if start < base
1456                && buffer.info[base].indic_position() as u32 > ot_position_t::POS_BASE_C as u32
1457            {
1458                base -= 1;
1459            }
1460
1461            break;
1462        }
1463
1464        base += 1;
1465    }
1466
1467    if base == end
1468        && start < base
1469        && buffer.info[base - 1].is_one_of(rb_flag(ot_category_t::OT_ZWJ as u32))
1470    {
1471        base -= 1;
1472    }
1473
1474    if base < end {
1475        while start < base
1476            && buffer.info[base].is_one_of(
1477                rb_flag(ot_category_t::OT_N as u32) | rb_flag(ot_category_t::OT_H as u32),
1478            )
1479        {
1480            base -= 1;
1481        }
1482    }
1483
1484    // - Reorder matras:
1485    //
1486    //   If a pre-base matra character had been reordered before applying basic
1487    //   features, the glyph can be moved closer to the main consonant based on
1488    //   whether half-forms had been formed. Actual position for the matra is
1489    //   defined as “after last standalone halant glyph, after initial matra
1490    //   position and before the main consonant”. If ZWJ or ZWNJ follow this
1491    //   halant, position is moved after it.
1492    //
1493    // IMPLEMENTATION NOTES:
1494    //
1495    // It looks like the last sentence is wrong.  Testing, with Windows 7 Uniscribe
1496    // and Devanagari shows that the behavior is best described as:
1497    //
1498    // "If ZWJ follows this halant, matra is NOT repositioned after this halant.
1499    //  If ZWNJ follows this halant, position is moved after it."
1500    //
1501    // Test case, with Adobe Devanagari or Nirmala UI:
1502    //
1503    //   U+091F,U+094D,U+200C,U+092F,U+093F
1504    //   (Matra moves to the middle, after ZWNJ.)
1505    //
1506    //   U+091F,U+094D,U+200D,U+092F,U+093F
1507    //   (Matra does NOT move, stays to the left.)
1508    //
1509    // https://github.com/harfbuzz/harfbuzz/issues/1070
1510
1511    // Otherwise there can't be any pre-base matra characters.
1512    if start + 1 < end && start < base {
1513        // If we lost track of base, alas, position before last thingy.
1514        let mut new_pos = if base == end { base - 2 } else { base - 1 };
1515
1516        // Malayalam / Tamil do not have "half" forms or explicit virama forms.
1517        // The glyphs formed by 'half' are Chillus or ligated explicit viramas.
1518        // We want to position matra after them.
1519        if buffer.script != Some(script::MALAYALAM) && buffer.script != Some(script::TAMIL) {
1520            loop {
1521                while new_pos > start
1522                    && !buffer.info[new_pos].is_one_of(
1523                        rb_flag(ot_category_t::OT_M as u32)
1524                            | rb_flag(ot_category_t::OT_MPst as u32)
1525                            | rb_flag(ot_category_t::OT_H as u32),
1526                    )
1527                {
1528                    new_pos -= 1;
1529                }
1530
1531                // If we found no Halant we are done.
1532                // Otherwise only proceed if the Halant does
1533                // not belong to the Matra itself!
1534                if buffer.info[new_pos].is_halant()
1535                    && buffer.info[new_pos].indic_position() != ot_position_t::POS_PRE_M
1536                {
1537                    if new_pos + 1 < end {
1538                        // -> If ZWJ follows this halant, matra is NOT repositioned after this halant.
1539                        if buffer.info[new_pos + 1].indic_category() == ot_category_t::OT_ZWJ {
1540                            // Keep searching.
1541                            if new_pos > start {
1542                                new_pos -= 1;
1543                                continue;
1544                            }
1545                        }
1546
1547                        // -> If ZWNJ follows this halant, position is moved after it.
1548                        //
1549                        // IMPLEMENTATION NOTES:
1550                        //
1551                        // This is taken care of by the state-machine. A Halant,ZWNJ is a terminating
1552                        // sequence for a consonant syllable; any pre-base matras occurring after it
1553                        // will belong to the subsequent syllable.
1554                    }
1555                } else {
1556                    new_pos = start; // No move.
1557                }
1558
1559                break;
1560            }
1561        }
1562
1563        if start < new_pos && buffer.info[new_pos].indic_position() != ot_position_t::POS_PRE_M {
1564            // Now go see if there's actually any matras...
1565            for i in (start + 1..=new_pos).rev() {
1566                if buffer.info[i - 1].indic_position() == ot_position_t::POS_PRE_M {
1567                    let old_pos = i - 1;
1568                    // Shouldn't actually happen.
1569                    if old_pos < base && base <= new_pos {
1570                        base -= 1;
1571                    }
1572
1573                    let tmp = buffer.info[old_pos];
1574                    for i in 0..new_pos - old_pos {
1575                        buffer.info[i + old_pos] = buffer.info[i + old_pos + 1];
1576                    }
1577                    buffer.info[new_pos] = tmp;
1578
1579                    // Note: this merge_clusters() is intentionally *after* the reordering.
1580                    // Indic matra reordering is special and tricky...
1581                    buffer.merge_clusters(new_pos, cmp::min(end, base + 1));
1582
1583                    new_pos -= 1;
1584                }
1585            }
1586        } else {
1587            for i in start..base {
1588                if buffer.info[i].indic_position() == ot_position_t::POS_PRE_M {
1589                    buffer.merge_clusters(i, cmp::min(end, base + 1));
1590                    break;
1591                }
1592            }
1593        }
1594    }
1595
1596    // - Reorder reph:
1597    //
1598    //   Reph’s original position is always at the beginning of the syllable,
1599    //   (i.e. it is not reordered at the character reordering stage). However,
1600    //   it will be reordered according to the basic-forms shaping results.
1601    //   Possible positions for reph, depending on the script, are; after main,
1602    //   before post-base consonant forms, and after post-base consonant forms.
1603
1604    // Two cases:
1605    //
1606    // - If repha is encoded as a sequence of characters (Ra,H or Ra,H,ZWJ), then
1607    //   we should only move it if the sequence ligated to the repha form.
1608    //
1609    // - If repha is encoded separately and in the logical position, we should only
1610    //   move it if it did NOT ligate.  If it ligated, it's probably the font trying
1611    //   to make it work without the reordering.
1612
1613    if start + 1 < end
1614        && buffer.info[start].indic_position() == ot_position_t::POS_RA_TO_BECOME_REPH
1615        && (buffer.info[start].indic_category() == ot_category_t::OT_Repha)
1616            ^ buffer.info[start].ligated_and_didnt_multiply()
1617    {
1618        let mut new_reph_pos;
1619        'reph: {
1620            let reph_pos = indic_plan.config.reph_pos;
1621
1622            // 1. If reph should be positioned after post-base consonant forms,
1623            //    proceed to step 5.
1624            if reph_pos != RephPosition::AfterPost {
1625                // 2. If the reph repositioning class is not after post-base: target
1626                //    position is after the first explicit halant glyph between the
1627                //    first post-reph consonant and last main consonant. If ZWJ or ZWNJ
1628                //    are following this halant, position is moved after it. If such
1629                //    position is found, this is the target position. Otherwise,
1630                //    proceed to the next step.
1631                //
1632                //    Note: in old-implementation fonts, where classifications were
1633                //    fixed in shaping engine, there was no case where reph position
1634                //    will be found on this step.
1635                {
1636                    new_reph_pos = start + 1;
1637                    while new_reph_pos < base && !buffer.info[new_reph_pos].is_halant() {
1638                        new_reph_pos += 1;
1639                    }
1640
1641                    if new_reph_pos < base && buffer.info[new_reph_pos].is_halant() {
1642                        // ->If ZWJ or ZWNJ are following this halant, position is moved after it.
1643                        if new_reph_pos + 1 < base && buffer.info[new_reph_pos + 1].is_joiner() {
1644                            new_reph_pos += 1;
1645                        }
1646
1647                        break 'reph;
1648                    }
1649                }
1650
1651                // 3. If reph should be repositioned after the main consonant: find the
1652                //    first consonant not ligated with main, or find the first
1653                //    consonant that is not a potential pre-base-reordering Ra.
1654                if reph_pos == RephPosition::AfterMain {
1655                    new_reph_pos = base;
1656                    while new_reph_pos + 1 < end
1657                        && buffer.info[new_reph_pos + 1].indic_position()
1658                            <= ot_position_t::POS_AFTER_MAIN
1659                    {
1660                        new_reph_pos += 1;
1661                    }
1662
1663                    if new_reph_pos < end {
1664                        break 'reph;
1665                    }
1666                }
1667
1668                // 4. If reph should be positioned before post-base consonant, find
1669                //    first post-base classified consonant not ligated with main. If no
1670                //    consonant is found, the target position should be before the
1671                //    first matra, syllable modifier sign or vedic sign.
1672                //
1673                // This is our take on what step 4 is trying to say (and failing, BADLY).
1674                if reph_pos == RephPosition::AfterSub {
1675                    new_reph_pos = base;
1676                    while new_reph_pos + 1 < end
1677                        && (rb_flag_unsafe(buffer.info[new_reph_pos + 1].indic_position() as u32)
1678                            & (rb_flag(ot_position_t::POS_POST_C as u32)
1679                                | rb_flag(ot_position_t::POS_AFTER_POST as u32)
1680                                | rb_flag(ot_position_t::POS_SMVD as u32)))
1681                            == 0
1682                    {
1683                        new_reph_pos += 1;
1684                    }
1685
1686                    if new_reph_pos < end {
1687                        break 'reph;
1688                    }
1689                }
1690            }
1691
1692            // 5. If no consonant is found in steps 3 or 4, move reph to a position
1693            //    immediately before the first post-base matra, syllable modifier
1694            //    sign or vedic sign that has a reordering class after the intended
1695            //    reph position. For example, if the reordering position for reph
1696            //    is post-main, it will skip above-base matras that also have a
1697            //    post-main position.
1698            //
1699            // Copied from step 2.
1700            new_reph_pos = start + 1;
1701            while new_reph_pos < base && !buffer.info[new_reph_pos].is_halant() {
1702                new_reph_pos += 1;
1703            }
1704
1705            if new_reph_pos < base && buffer.info[new_reph_pos].is_halant() {
1706                /* ->If ZWJ or ZWNJ are following this halant, position is moved after it. */
1707                if new_reph_pos + 1 < base && buffer.info[new_reph_pos + 1].is_joiner() {
1708                    new_reph_pos += 1;
1709                }
1710
1711                break 'reph;
1712            }
1713            // See https://github.com/harfbuzz/harfbuzz/issues/2298#issuecomment-615318654
1714
1715            // 6. Otherwise, reorder reph to the end of the syllable.
1716            {
1717                new_reph_pos = end - 1;
1718                while new_reph_pos > start
1719                    && buffer.info[new_reph_pos].indic_position() == ot_position_t::POS_SMVD
1720                {
1721                    new_reph_pos -= 1;
1722                }
1723
1724                // If the Reph is to be ending up after a Matra,Halant sequence,
1725                // position it before that Halant so it can interact with the Matra.
1726                // However, if it's a plain Consonant,Halant we shouldn't do that.
1727                // Uniscribe doesn't do this.
1728                // TEST: U+0930,U+094D,U+0915,U+094B,U+094D
1729                if buffer.info[new_reph_pos].is_halant() {
1730                    for info in &buffer.info[base + 1..new_reph_pos] {
1731                        if (rb_flag_unsafe(info.indic_category() as u32)
1732                            & (rb_flag(ot_category_t::OT_M as u32)
1733                                | rb_flag(ot_category_t::OT_MPst as u32)))
1734                            != 0
1735                        {
1736                            // Ok, got it.
1737                            new_reph_pos -= 1;
1738                        }
1739                    }
1740                }
1741            }
1742
1743            break 'reph;
1744        }
1745
1746        // Move
1747        buffer.merge_clusters(start, new_reph_pos + 1);
1748
1749        let reph = buffer.info[start];
1750        for i in 0..new_reph_pos - start {
1751            buffer.info[i + start] = buffer.info[i + start + 1];
1752        }
1753        buffer.info[new_reph_pos] = reph;
1754
1755        if start < base && base <= new_reph_pos {
1756            base -= 1;
1757        }
1758    }
1759
1760    // - Reorder pre-base-reordering consonants:
1761    //
1762    //   If a pre-base-reordering consonant is found, reorder it according to
1763    //   the following rules:
1764
1765    // Otherwise there can't be any pre-base-reordering Ra.
1766    if try_pref && base + 1 < end {
1767        for i in base + 1..end {
1768            if (buffer.info[i].mask & indic_plan.mask_array[indic_feature::PREF]) != 0 {
1769                // 1. Only reorder a glyph produced by substitution during application
1770                //    of the <pref> feature. (Note that a font may shape a Ra consonant with
1771                //    the feature generally but block it in certain contexts.)
1772                //
1773                // Note: We just check that something got substituted.  We don't check that
1774                // the <pref> feature actually did it...
1775                //
1776                // Reorder pref only if it ligated.
1777                if buffer.info[i].ligated_and_didnt_multiply() {
1778                    // 2. Try to find a target position the same way as for pre-base matra.
1779                    //    If it is found, reorder pre-base consonant glyph.
1780                    //
1781                    // 3. If position is not found, reorder immediately before main consonant.
1782
1783                    let mut new_pos = base;
1784                    // Malayalam / Tamil do not have "half" forms or explicit virama forms.
1785                    // The glyphs formed by 'half' are Chillus or ligated explicit viramas.
1786                    // We want to position matra after them.
1787                    if buffer.script != Some(script::MALAYALAM)
1788                        && buffer.script != Some(script::TAMIL)
1789                    {
1790                        while new_pos > start
1791                            && !buffer.info[new_pos - 1].is_one_of(
1792                                rb_flag(ot_category_t::OT_M as u32)
1793                                    | rb_flag(ot_category_t::OT_MPst as u32)
1794                                    | rb_flag(ot_category_t::OT_H as u32),
1795                            )
1796                        {
1797                            new_pos -= 1;
1798                        }
1799                    }
1800
1801                    if new_pos > start && buffer.info[new_pos - 1].is_halant() {
1802                        // -> If ZWJ or ZWNJ follow this halant, position is moved after it.
1803                        if new_pos < end && buffer.info[new_pos].is_joiner() {
1804                            new_pos += 1;
1805                        }
1806                    }
1807
1808                    {
1809                        let old_pos = i;
1810
1811                        buffer.merge_clusters(new_pos, old_pos + 1);
1812                        let tmp = buffer.info[old_pos];
1813                        for i in (0..old_pos - new_pos).rev() {
1814                            buffer.info[i + new_pos + 1] = buffer.info[i + new_pos];
1815                        }
1816                        buffer.info[new_pos] = tmp;
1817
1818                        if new_pos <= base && base < old_pos {
1819                            // TODO: investigate
1820                            #[allow(unused_assignments)]
1821                            {
1822                                base += 1;
1823                            }
1824                        }
1825                    }
1826                }
1827
1828                break;
1829            }
1830        }
1831    }
1832
1833    // Apply 'init' to the Left Matra if it's a word start.
1834    if buffer.info[start].indic_position() == ot_position_t::POS_PRE_M {
1835        if start == 0
1836            || (rb_flag_unsafe(buffer.info[start - 1].general_category().to_u8() as u32)
1837                & rb_flag_range(
1838                    hb_gc::HB_UNICODE_GENERAL_CATEGORY_FORMAT,
1839                    hb_gc::HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK,
1840                ))
1841                == 0
1842        {
1843            buffer.info[start].mask |= indic_plan.mask_array[indic_feature::INIT];
1844        } else {
1845            buffer.unsafe_to_break(Some(start - 1), Some(start + 1));
1846        }
1847    }
1848}