Skip to main content

harfrust/hb/
ot_shaper_arabic.rs

1use super::algs::*;
2use super::buffer::*;
3use super::font_funcs::FontFuncsDispatch;
4use super::ot_map::*;
5use super::ot_shape::*;
6use super::ot_shape_normalize::HB_OT_SHAPE_NORMALIZATION_MODE_AUTO;
7use super::ot_shape_plan::hb_ot_shape_plan_t;
8use super::ot_shaper::*;
9use super::unicode::*;
10use super::{hb_mask_t, hb_tag_t, script, GlyphInfo, Script};
11use crate::Direction;
12use alloc::boxed::Box;
13
14const HB_BUFFER_SCRATCH_FLAG_ARABIC_HAS_STCH: hb_buffer_scratch_flags_t =
15    HB_BUFFER_SCRATCH_FLAG_SHAPER0;
16
17// See:
18// https://github.com/harfbuzz/harfbuzz/commit/6e6f82b6f3dde0fc6c3c7d991d9ec6cfff57823d#commitcomment-14248516
19fn is_word_category(gc: GeneralCategory) -> bool {
20    (rb_flag_unsafe(gc.to_u8() as u32)
21        & (rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED)
22            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_PRIVATE_USE)
23            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_MODIFIER_LETTER)
24            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_OTHER_LETTER)
25            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK)
26            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK)
27            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
28            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_DECIMAL_NUMBER)
29            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_LETTER_NUMBER)
30            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_OTHER_NUMBER)
31            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_CURRENCY_SYMBOL)
32            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_MODIFIER_SYMBOL)
33            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_MATH_SYMBOL)
34            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL)))
35        != 0
36}
37
38#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
39pub enum hb_arabic_joining_type_t {
40    U = 0,
41    L = 1,
42    R = 2,
43    D = 3,
44    // We don't have C, like harfbuzz, because Rust doesn't allow duplicated enum variants.
45    GroupAlaph = 4,
46    GroupDalathRish = 5,
47    T = 6,
48    X = 7, // means: use general-category to choose between U or T.
49}
50
51fn get_joining_type(u: Codepoint, gc: GeneralCategory) -> hb_arabic_joining_type_t {
52    let j_type = super::ot_shaper_arabic_table::joining_type(u);
53    if j_type != hb_arabic_joining_type_t::X {
54        return j_type;
55    }
56
57    let ok = rb_flag_unsafe(gc.to_u8() as u32)
58        & (rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
59            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK)
60            | rb_flag(hb_gc::HB_UNICODE_GENERAL_CATEGORY_FORMAT));
61
62    if ok != 0 {
63        hb_arabic_joining_type_t::T
64    } else {
65        hb_arabic_joining_type_t::U
66    }
67}
68
69fn feature_is_syriac(tag: hb_tag_t) -> bool {
70    matches!(tag.to_be_bytes()[3], b'2' | b'3')
71}
72
73const ARABIC_FEATURES: &[hb_tag_t] = &[
74    hb_tag_t::new(b"isol"),
75    hb_tag_t::new(b"fina"),
76    hb_tag_t::new(b"fin2"),
77    hb_tag_t::new(b"fin3"),
78    hb_tag_t::new(b"medi"),
79    hb_tag_t::new(b"med2"),
80    hb_tag_t::new(b"init"),
81];
82
83mod arabic_action_t {
84    pub const ISOL: u8 = 0;
85    pub const FINA: u8 = 1;
86    pub const FIN2: u8 = 2;
87    pub const FIN3: u8 = 3;
88    pub const MEDI: u8 = 4;
89    pub const MED2: u8 = 5;
90    pub const INIT: u8 = 6;
91    pub const NONE: u8 = 7;
92
93    // We abuse the same byte for other things...
94    pub const STRETCHING_FIXED: u8 = 8;
95    pub const STRETCHING_REPEATING: u8 = 9;
96
97    #[inline]
98    pub fn is_stch(n: u8) -> bool {
99        matches!(n, STRETCHING_FIXED | STRETCHING_REPEATING)
100    }
101}
102
103static STATE_TABLE: &[[(u8, u8, u16); 6]] = &[
104    // jt_U,          jt_L,          jt_R,
105    // jt_D,          jg_ALAPH,      jg_DALATH_RISH
106
107    // State 0: prev was U, not willing to join.
108    [
109        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
110        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
111        (arabic_action_t::NONE, arabic_action_t::ISOL, 1),
112        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
113        (arabic_action_t::NONE, arabic_action_t::ISOL, 1),
114        (arabic_action_t::NONE, arabic_action_t::ISOL, 6),
115    ],
116    // State 1: prev was R or action::ISOL/ALAPH, not willing to join.
117    [
118        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
119        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
120        (arabic_action_t::NONE, arabic_action_t::ISOL, 1),
121        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
122        (arabic_action_t::NONE, arabic_action_t::FIN2, 5),
123        (arabic_action_t::NONE, arabic_action_t::ISOL, 6),
124    ],
125    // State 2: prev was D/L in action::ISOL form, willing to join.
126    [
127        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
128        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
129        (arabic_action_t::INIT, arabic_action_t::FINA, 1),
130        (arabic_action_t::INIT, arabic_action_t::FINA, 3),
131        (arabic_action_t::INIT, arabic_action_t::FINA, 4),
132        (arabic_action_t::INIT, arabic_action_t::FINA, 6),
133    ],
134    // State 3: prev was D in action::FINA form, willing to join.
135    [
136        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
137        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
138        (arabic_action_t::MEDI, arabic_action_t::FINA, 1),
139        (arabic_action_t::MEDI, arabic_action_t::FINA, 3),
140        (arabic_action_t::MEDI, arabic_action_t::FINA, 4),
141        (arabic_action_t::MEDI, arabic_action_t::FINA, 6),
142    ],
143    // State 4: prev was action::FINA ALAPH, not willing to join.
144    [
145        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
146        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
147        (arabic_action_t::MED2, arabic_action_t::ISOL, 1),
148        (arabic_action_t::MED2, arabic_action_t::ISOL, 2),
149        (arabic_action_t::MED2, arabic_action_t::FIN2, 5),
150        (arabic_action_t::MED2, arabic_action_t::ISOL, 6),
151    ],
152    // State 5: prev was FIN2/FIN3 ALAPH, not willing to join.
153    [
154        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
155        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
156        (arabic_action_t::ISOL, arabic_action_t::ISOL, 1),
157        (arabic_action_t::ISOL, arabic_action_t::ISOL, 2),
158        (arabic_action_t::ISOL, arabic_action_t::FIN2, 5),
159        (arabic_action_t::ISOL, arabic_action_t::ISOL, 6),
160    ],
161    // State 6: prev was DALATH/RISH, not willing to join.
162    [
163        (arabic_action_t::NONE, arabic_action_t::NONE, 0),
164        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
165        (arabic_action_t::NONE, arabic_action_t::ISOL, 1),
166        (arabic_action_t::NONE, arabic_action_t::ISOL, 2),
167        (arabic_action_t::NONE, arabic_action_t::FIN3, 5),
168        (arabic_action_t::NONE, arabic_action_t::ISOL, 6),
169    ],
170];
171
172impl GlyphInfo {
173    declare_buffer_var_alias!(
174        OT_SHAPER_VAR_U8_AUXILIARY_VAR,
175        u8,
176        ARABIC_SHAPING_ACTION_VAR,
177        arabic_shaping_action,
178        set_arabic_shaping_action
179    );
180}
181
182fn deallocate_buffer_var(
183    _: &hb_ot_shape_plan_t,
184    _: &mut FontFuncsDispatch,
185    buffer: &mut hb_buffer_t,
186) -> bool {
187    buffer.deallocate_var(GlyphInfo::ARABIC_SHAPING_ACTION_VAR);
188
189    false
190}
191
192fn collect_features(planner: &mut hb_ot_shape_planner_t) {
193    // We apply features according to the Arabic spec, with pauses
194    // in between most.
195    //
196    // The pause between init/medi/... and rlig is required.  See eg:
197    // https://bugzilla.mozilla.org/show_bug.cgi?id=644184
198    //
199    // The pauses between init/medi/... themselves are not necessarily
200    // needed as only one of those features is applied to any character.
201    // The only difference it makes is when fonts have contextual
202    // substitutions.  We now follow the order of the spec, which makes
203    // for better experience if that's what Uniscribe is doing.
204    //
205    // At least for Arabic, looks like Uniscribe has a pause between
206    // rlig and calt.  Otherwise the IranNastaliq's ALLAH ligature won't
207    // work.  However, testing shows that rlig and calt are applied
208    // together for Mongolian in Uniscribe.  As such, we only add a
209    // pause for Arabic, not other scripts.
210    //
211    // A pause after calt is required to make KFGQPC Uthmanic Script HAFS
212    // work correctly.  See https://github.com/harfbuzz/harfbuzz/issues/505
213
214    planner
215        .ot_map
216        .enable_feature(hb_tag_t::new(b"stch"), F_NONE, 1);
217    planner.ot_map.add_gsub_pause(Some(record_stch));
218
219    planner
220        .ot_map
221        .enable_feature(hb_tag_t::new(b"ccmp"), F_MANUAL_ZWJ, 1);
222    planner
223        .ot_map
224        .enable_feature(hb_tag_t::new(b"locl"), F_MANUAL_ZWJ, 1);
225
226    planner.ot_map.add_gsub_pause(None);
227
228    for feature in ARABIC_FEATURES {
229        let has_fallback = planner.script == Some(script::ARABIC) && !feature_is_syriac(*feature);
230        let flags = if has_fallback { F_HAS_FALLBACK } else { F_NONE };
231        planner
232            .ot_map
233            .add_feature(*feature, F_MANUAL_ZWJ | flags, 1);
234        planner.ot_map.add_gsub_pause(None);
235    }
236    planner.ot_map.add_gsub_pause(Some(deallocate_buffer_var));
237
238    // Normally, Unicode says a ZWNJ means "don't ligate".  In Arabic script
239    // however, it says a ZWJ should also mean "don't ligate".  So we run
240    // the main ligating features as MANUAL_ZWJ.
241
242    planner
243        .ot_map
244        .enable_feature(hb_tag_t::new(b"rlig"), F_MANUAL_ZWJ | F_HAS_FALLBACK, 1);
245
246    if planner.script == Some(script::ARABIC) {
247        planner.ot_map.add_gsub_pause(Some(arabic_fallback_shape));
248    }
249
250    // No pause after rclt.
251    // See 98460779bae19e4d64d29461ff154b3527bf8420
252    planner
253        .ot_map
254        .enable_feature(hb_tag_t::new(b"calt"), F_MANUAL_ZWJ, 1);
255    /* https://github.com/harfbuzz/harfbuzz/issues/1573 */
256    if !planner.ot_map.has_feature(hb_tag_t::new(b"rclt")) {
257        planner.ot_map.add_gsub_pause(None);
258    }
259
260    planner
261        .ot_map
262        .enable_feature(hb_tag_t::new(b"liga"), F_MANUAL_ZWJ, 1);
263    planner
264        .ot_map
265        .enable_feature(hb_tag_t::new(b"clig"), F_MANUAL_ZWJ, 1);
266
267    // The spec includes 'cswh'.  Earlier versions of Windows
268    // used to enable this by default, but testing suggests
269    // that Windows 8 and later do not enable it by default,
270    // and spec now says 'Off by default'.
271    // We disabled this in ae23c24c32.
272    // Note that IranNastaliq uses this feature extensively
273    // to fixup broken glyph sequences.  Oh well...
274    // Test case: U+0643,U+0640,U+0631.
275
276    // planner.ot_map.enable_feature(feature::CONTEXTUAL_SWASH, F_MANUAL_ZWJ, 1);
277    planner
278        .ot_map
279        .enable_feature(hb_tag_t::new(b"mset"), F_MANUAL_ZWJ, 1);
280}
281
282pub struct arabic_shape_plan_t {
283    // The "+ 1" in the next array is to accommodate for the "NONE" command,
284    // which is not an OpenType feature, but this simplifies the code by not
285    // having to do a "if (... < NONE) ..." and just rely on the fact that
286    // mask_array[NONE] == 0.
287    mask_array: [hb_mask_t; ARABIC_FEATURES.len() + 1],
288    has_stch: bool,
289}
290
291pub fn data_create_arabic(plan: &hb_ot_shape_plan_t) -> arabic_shape_plan_t {
292    let has_stch = plan.ot_map.get_1_mask(hb_tag_t::new(b"stch")) != 0;
293
294    let mut mask_array = [0; ARABIC_FEATURES.len() + 1];
295    for i in 0..ARABIC_FEATURES.len() {
296        mask_array[i] = plan.ot_map.get_1_mask(ARABIC_FEATURES[i]);
297    }
298
299    arabic_shape_plan_t {
300        mask_array,
301        has_stch,
302    }
303}
304
305fn arabic_joining(buffer: &mut hb_buffer_t) {
306    let mut prev: Option<usize> = None;
307    let mut state = 0;
308
309    // Check pre-context.
310    for i in 0..buffer.context_len[0] {
311        let c = buffer.context[0][i] as Codepoint;
312        let this_type = get_joining_type(c, c.general_category());
313        if this_type == hb_arabic_joining_type_t::T {
314            continue;
315        }
316
317        state = STATE_TABLE[state][this_type as usize].2 as usize;
318        break;
319    }
320
321    for i in 0..buffer.len {
322        let this_type = get_joining_type(
323            buffer.info[i].as_codepoint(),
324            buffer.info[i].general_category(),
325        );
326        if this_type == hb_arabic_joining_type_t::T {
327            buffer.info[i].set_arabic_shaping_action(arabic_action_t::NONE);
328            continue;
329        }
330
331        let entry = &STATE_TABLE[state][this_type as usize];
332        if entry.0 != arabic_action_t::NONE && prev.is_some() {
333            if let Some(prev) = prev {
334                buffer.info[prev].set_arabic_shaping_action(entry.0);
335                buffer.safe_to_insert_tatweel(Some(prev), Some(i + 1));
336            }
337        }
338        // States that have a possible prev_action.
339        else {
340            if let Some(prev) = prev {
341                if this_type >= hb_arabic_joining_type_t::R || (2 <= state && state <= 5) {
342                    buffer.unsafe_to_concat(Some(prev), Some(i + 1));
343                }
344            } else {
345                if this_type >= hb_arabic_joining_type_t::R {
346                    buffer.unsafe_to_concat_from_outbuffer(Some(0), Some(i + 1));
347                }
348            }
349        }
350
351        buffer.info[i].set_arabic_shaping_action(entry.1);
352
353        prev = Some(i);
354        state = entry.2 as usize;
355    }
356
357    for i in 0..buffer.context_len[1] {
358        let c = buffer.context[1][i] as Codepoint;
359        let this_type = get_joining_type(c, c.general_category());
360        if this_type == hb_arabic_joining_type_t::T {
361            continue;
362        }
363
364        let entry = &STATE_TABLE[state][this_type as usize];
365        if entry.0 != arabic_action_t::NONE && prev.is_some() {
366            if let Some(prev) = prev {
367                buffer.info[prev].set_arabic_shaping_action(entry.0);
368                buffer.safe_to_insert_tatweel(Some(prev), Some(buffer.len));
369            }
370        }
371        // States that have a possible prev_action.
372        else if 2 <= state && state <= 5 {
373            if let Some(prev) = prev {
374                buffer.unsafe_to_concat(Some(prev), Some(buffer.len));
375            }
376        }
377
378        break;
379    }
380}
381
382fn mongolian_variation_selectors(buffer: &mut hb_buffer_t) {
383    // Copy arabic_shaping_action() from base to Mongolian variation selectors.
384    let len = buffer.len;
385    let info = &mut buffer.info;
386    for i in 1..len {
387        if (0x180B..=0x180D).contains(&info[i].glyph_id) || info[i].glyph_id == 0x180F {
388            let a = info[i - 1].arabic_shaping_action();
389            info[i].set_arabic_shaping_action(a);
390        }
391    }
392}
393
394fn setup_masks_arabic_plan(
395    plan: &hb_ot_shape_plan_t,
396    _: &mut FontFuncsDispatch,
397    buffer: &mut hb_buffer_t,
398) {
399    buffer.allocate_var(GlyphInfo::ARABIC_SHAPING_ACTION_VAR);
400
401    let arabic_plan = plan.data::<arabic_shape_plan_t>();
402    setup_masks_inner(arabic_plan, plan.script, buffer);
403}
404
405pub fn setup_masks_inner(
406    arabic_plan: &arabic_shape_plan_t,
407    script: Option<Script>,
408    buffer: &mut hb_buffer_t,
409) {
410    arabic_joining(buffer);
411    if script == Some(script::MONGOLIAN) {
412        mongolian_variation_selectors(buffer);
413    }
414
415    for info in buffer.info_slice_mut() {
416        info.mask |= arabic_plan.mask_array[info.arabic_shaping_action() as usize];
417    }
418}
419
420fn arabic_fallback_shape(
421    _: &hb_ot_shape_plan_t,
422    _: &mut FontFuncsDispatch,
423    _: &mut hb_buffer_t,
424) -> bool {
425    false
426}
427
428// Stretch feature: "stch".
429// See example here:
430// https://docs.microsoft.com/en-us/typography/script-development/syriac
431// We implement this in a generic way, such that the Arabic subtending
432// marks can use it as well.
433fn record_stch(
434    plan: &hb_ot_shape_plan_t,
435    _: &mut FontFuncsDispatch,
436    buffer: &mut hb_buffer_t,
437) -> bool {
438    let arabic_plan = plan.data::<arabic_shape_plan_t>();
439    if !arabic_plan.has_stch {
440        return false;
441    }
442
443    // 'stch' feature was just applied.  Look for anything that multiplied,
444    // and record it for stch treatment later.  Note that rtlm, frac, etc
445    // are applied before stch, but we assume that they didn't result in
446    // anything multiplying into 5 pieces, so it's safe-ish...
447
448    let len = buffer.len;
449    let info = &mut buffer.info;
450    let mut has_stch = false;
451    for glyph_info in &mut info[..len] {
452        if glyph_info.multiplied() {
453            let comp = if glyph_info.lig_comp() % 2 != 0 {
454                arabic_action_t::STRETCHING_REPEATING
455            } else {
456                arabic_action_t::STRETCHING_FIXED
457            };
458
459            glyph_info.set_arabic_shaping_action(comp);
460            has_stch = true;
461        }
462    }
463
464    if has_stch {
465        buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_ARABIC_HAS_STCH;
466    }
467
468    false
469}
470
471fn apply_stch(face: &mut FontFuncsDispatch, buffer: &mut hb_buffer_t) {
472    if buffer.scratch_flags & HB_BUFFER_SCRATCH_FLAG_ARABIC_HAS_STCH == 0 {
473        return;
474    }
475
476    let rtl = buffer.direction == Direction::RightToLeft;
477
478    if !rtl {
479        buffer.reverse();
480    }
481
482    // We do a two pass implementation:
483    // First pass calculates the exact number of extra glyphs we need,
484    // We then enlarge buffer to have that much room,
485    // Second pass applies the stretch, copying things to the end of buffer.
486
487    let mut extra_glyphs_needed: usize = 0; // Set during MEASURE, used during CUT
488    const MEASURE: usize = 0;
489    const CUT: usize = 1;
490
491    for step in 0..2 {
492        let new_len = buffer.len + extra_glyphs_needed; // write head during CUT
493        let mut i = buffer.len;
494        let mut j = new_len;
495        while i != 0 {
496            if !arabic_action_t::is_stch(buffer.info[i - 1].arabic_shaping_action()) {
497                if step == CUT {
498                    j -= 1;
499                    buffer.info[j] = buffer.info[i - 1];
500                    buffer.pos[j] = buffer.pos[i - 1];
501                }
502
503                i -= 1;
504                continue;
505            }
506
507            // Yay, justification!
508
509            let mut w_total = 0; // Total to be filled
510            let mut w_fixed = 0; // Sum of fixed tiles
511            let mut w_repeating = 0; // Sum of repeating tiles
512            let mut n_repeating: i32 = 0;
513
514            let end = i;
515            while i != 0 && arabic_action_t::is_stch(buffer.info[i - 1].arabic_shaping_action()) {
516                i -= 1;
517                let width = face.advance_width(buffer.info[i].as_glyph());
518
519                if buffer.info[i].arabic_shaping_action() == arabic_action_t::STRETCHING_FIXED {
520                    w_fixed += width;
521                } else {
522                    w_repeating += width;
523                    n_repeating += 1;
524                }
525            }
526
527            let start = i;
528            let mut context = i;
529            while context != 0
530                && !arabic_action_t::is_stch(buffer.info[context - 1].arabic_shaping_action())
531                && (buffer.info[context - 1].is_default_ignorable()
532                    || is_word_category(buffer.info[context - 1].general_category()))
533            {
534                context -= 1;
535                w_total += buffer.pos[context].x_advance;
536            }
537
538            i += 1; // Don't touch i again.
539
540            // Number of additional times to repeat each repeating tile.
541            let mut n_copies: i32 = 0;
542
543            let mut w_remaining = w_total - w_fixed;
544            if w_remaining > w_repeating && w_repeating > 0 {
545                n_copies = w_remaining / (w_repeating) - 1;
546            }
547
548            // See if we can improve the fit by adding an extra repeat and squeezing them together a bit.
549            let mut extra_repeat_overlap = 0;
550            let shortfall = w_remaining - w_repeating * (n_copies + 1);
551            if shortfall > 0 && n_repeating > 0 {
552                n_copies += 1;
553                let excess = (n_copies + 1) * w_repeating - w_remaining;
554                if excess > 0 {
555                    extra_repeat_overlap = excess / (n_copies * n_repeating);
556                    w_remaining = 0;
557                }
558            }
559
560            if step == MEASURE {
561                extra_glyphs_needed += (n_copies * n_repeating) as usize;
562            } else {
563                buffer.unsafe_to_break(Some(context), Some(end));
564                let mut x_offset = w_remaining / 2;
565                for k in (start + 1..=end).rev() {
566                    let width = face.advance_width(buffer.info[k - 1].as_glyph());
567
568                    let mut repeat = 1;
569                    if buffer.info[k - 1].arabic_shaping_action()
570                        == arabic_action_t::STRETCHING_REPEATING
571                    {
572                        repeat += n_copies;
573                    }
574
575                    buffer.pos[k - 1].x_advance = 0;
576
577                    for n in 0..repeat {
578                        if rtl {
579                            x_offset -= width;
580                            if n > 0 {
581                                x_offset += extra_repeat_overlap;
582                            }
583                        }
584
585                        buffer.pos[k - 1].x_offset = x_offset;
586
587                        // Append copy.
588                        j -= 1;
589                        buffer.info[j] = buffer.info[k - 1];
590                        buffer.pos[j] = buffer.pos[k - 1];
591
592                        if !rtl {
593                            x_offset += width;
594
595                            if n > 0 {
596                                x_offset -= extra_repeat_overlap;
597                            }
598                        }
599                    }
600                }
601            }
602
603            i -= 1;
604        }
605
606        if step == MEASURE {
607            if !buffer.ensure(buffer.len + extra_glyphs_needed) {
608                break;
609            }
610        } else {
611            debug_assert_eq!(j, 0);
612            buffer.len = new_len;
613        }
614    }
615
616    if !rtl {
617        buffer.reverse();
618    }
619}
620
621fn postprocess_glyphs_arabic(
622    _: &hb_ot_shape_plan_t,
623    face: &mut FontFuncsDispatch,
624    buffer: &mut hb_buffer_t,
625) {
626    apply_stch(face, buffer);
627}
628
629// http://www.unicode.org/reports/tr53/
630static MODIFIER_COMBINING_MARKS: &[u32] = &[
631    0x0654, // ARABIC HAMZA ABOVE
632    0x0655, // ARABIC HAMZA BELOW
633    0x0658, // ARABIC MARK NOON GHUNNA
634    0x06DC, // ARABIC SMALL HIGH SEEN
635    0x06E3, // ARABIC SMALL LOW SEEN
636    0x06E7, // ARABIC SMALL HIGH YEH
637    0x06E8, // ARABIC SMALL HIGH NOON
638    0x08CA, // ARABIC SMALL HIGH FARSI YEH
639    0x08CB, // ARABIC SMALL HIGH YEH BARREE WITH TWO DOTS BELOW
640    0x08CD, // ARABIC SMALL HIGH ZAH
641    0x08CE, // ARABIC LARGE ROUND DOT ABOVE
642    0x08CF, // ARABIC LARGE ROUND DOT BELOW
643    0x08D3, // ARABIC SMALL LOW WAW
644    0x08F3, // ARABIC SMALL HIGH WAW
645];
646
647fn reorder_marks_arabic(
648    _: &hb_ot_shape_plan_t,
649    buffer: &mut hb_buffer_t,
650    mut start: usize,
651    end: usize,
652) {
653    let mut i = start;
654    for cc in [220u8, 230] {
655        while i < end && buffer.info[i].modified_combining_class() < cc {
656            i += 1;
657        }
658
659        if i == end {
660            break;
661        }
662
663        if buffer.info[i].modified_combining_class() > cc {
664            continue;
665        }
666
667        let mut j = i;
668        while j < end
669            && buffer.info[j].modified_combining_class() == cc
670            && MODIFIER_COMBINING_MARKS.contains(&buffer.info[j].glyph_id)
671        {
672            j += 1;
673        }
674
675        if i == j {
676            continue;
677        }
678
679        // Shift it!
680        let mut temp = [GlyphInfo::default(); MAX_COMBINING_MARKS];
681        debug_assert!(j - i <= MAX_COMBINING_MARKS);
682        buffer.merge_clusters(start, j);
683
684        temp[..j - i].copy_from_slice(&buffer.info[i..j]);
685
686        for k in (0..i - start).rev() {
687            buffer.info[k + start + j - i] = buffer.info[k + start];
688        }
689
690        buffer.info[start..][..j - i].copy_from_slice(&temp[..j - i]);
691
692        // Renumber CC such that the reordered sequence is still sorted.
693        // 22 and 26 are chosen because they are smaller than all Arabic categories,
694        // and are folded back to 220/230 respectively during fallback mark positioning.
695        //
696        // We do this because the CGJ-handling logic in the normalizer relies on
697        // mark sequences having an increasing order even after this reordering.
698        // https://github.com/harfbuzz/harfbuzz/issues/554
699        // This, however, does break some obscure sequences, where the normalizer
700        // might compose a sequence that it should not.  For example, in the seequence
701        // ALEF, HAMZAH, MADDAH, we should NOT try to compose ALEF+MADDAH, but with this
702        // renumbering, we will.
703        let new_start = start + j - i;
704        let new_cc = if cc == 220 {
705            modified_combining_class::CCC22
706        } else {
707            modified_combining_class::CCC26
708        };
709
710        while start < new_start {
711            buffer.info[start].set_modified_combining_class(new_cc);
712            start += 1;
713        }
714
715        i = j;
716    }
717}
718
719pub const ARABIC_SHAPER: hb_ot_shaper_t = hb_ot_shaper_t {
720    collect_features: Some(collect_features),
721    override_features: None,
722    create_data: Some(|plan| Box::new(data_create_arabic(plan))),
723    preprocess_text: None,
724    postprocess_glyphs: Some(postprocess_glyphs_arabic),
725    normalization_preference: HB_OT_SHAPE_NORMALIZATION_MODE_AUTO,
726    decompose: None,
727    compose: None,
728    setup_masks: Some(setup_masks_arabic_plan),
729    gpos_tag: None,
730    reorder_marks: Some(reorder_marks_arabic),
731    zero_width_marks: HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_GDEF_LATE,
732    fallback_position: true,
733};