1
//! Implementation of font shaping for Arabic scripts
2
//!
3
//! Code herein follows the specification at:
4
//! <https://github.com/n8willis/opentype-shaping-documents/blob/master/opentype-shaping-arabic-general.md>
5

            
6
use unicode_joining_type::{get_joining_type, JoiningType};
7

            
8
use crate::error::{ParseError, ShapingError};
9
use crate::gsub::{self, Feature, FeatureMask, GlyphData, GlyphOrigin, RawGlyph};
10
use crate::layout::{FeatureTableSubstitution, GDEFTable, LayoutCache, LayoutTable, GSUB};
11
use crate::tag;
12
use crate::unicode::mcc::{
13
    modified_combining_class, sort_by_modified_combining_class, ModifiedCombiningClass,
14
};
15

            
16
#[derive(Clone)]
17
struct ArabicData {
18
    joining_type: JoiningType,
19
    feature_tag: u32,
20
}
21

            
22
impl GlyphData for ArabicData {
23
    fn merge(data1: ArabicData, _data2: ArabicData) -> ArabicData {
24
        // TODO hold off for future Unicode normalisation changes
25
        data1
26
    }
27
}
28

            
29
// Arabic glyphs are represented as `RawGlyph` structs with `ArabicData` for its `extra_data`.
30
type ArabicGlyph = RawGlyph<ArabicData>;
31

            
32
impl ArabicGlyph {
33
    fn is_transparent(&self) -> bool {
34
        self.extra_data.joining_type == JoiningType::Transparent || self.multi_subst_dup()
35
    }
36

            
37
    fn is_left_joining(&self) -> bool {
38
        self.extra_data.joining_type == JoiningType::LeftJoining
39
            || self.extra_data.joining_type == JoiningType::DualJoining
40
            || self.extra_data.joining_type == JoiningType::JoinCausing
41
    }
42

            
43
    fn is_right_joining(&self) -> bool {
44
        self.extra_data.joining_type == JoiningType::RightJoining
45
            || self.extra_data.joining_type == JoiningType::DualJoining
46
            || self.extra_data.joining_type == JoiningType::JoinCausing
47
    }
48

            
49
    fn feature_tag(&self) -> u32 {
50
        self.extra_data.feature_tag
51
    }
52

            
53
    fn set_feature_tag(&mut self, feature_tag: u32) {
54
        self.extra_data.feature_tag = feature_tag
55
    }
56
}
57

            
58
impl From<&RawGlyph<()>> for ArabicGlyph {
59
    fn from(raw_glyph: &RawGlyph<()>) -> ArabicGlyph {
60
        // Since there's no `Char` to work out the `ArabicGlyph`s joining type when the glyph's
61
        // `glyph_origin` is `GlyphOrigin::Direct`, we fallback to `JoiningType::NonJoining` as
62
        // the safest approach
63
        let joining_type = match raw_glyph.glyph_origin {
64
            GlyphOrigin::Char(c) => get_joining_type(c),
65
            GlyphOrigin::Direct => JoiningType::NonJoining,
66
        };
67

            
68
        ArabicGlyph {
69
            unicodes: raw_glyph.unicodes.clone(),
70
            glyph_index: raw_glyph.glyph_index,
71
            liga_component_pos: raw_glyph.liga_component_pos,
72
            glyph_origin: raw_glyph.glyph_origin,
73
            flags: raw_glyph.flags,
74
            variation: raw_glyph.variation,
75
            extra_data: ArabicData {
76
                joining_type,
77
                // For convenience, we loosely follow the spec (`2. Computing letter joining
78
                // states`) here by initialising all `ArabicGlyph`s to `tag::ISOL`
79
                feature_tag: tag::ISOL,
80
            },
81
        }
82
    }
83
}
84

            
85
impl From<&ArabicGlyph> for RawGlyph<()> {
86
    fn from(arabic_glyph: &ArabicGlyph) -> RawGlyph<()> {
87
        RawGlyph {
88
            unicodes: arabic_glyph.unicodes.clone(),
89
            glyph_index: arabic_glyph.glyph_index,
90
            liga_component_pos: arabic_glyph.liga_component_pos,
91
            glyph_origin: arabic_glyph.glyph_origin,
92
            flags: arabic_glyph.flags,
93
            variation: arabic_glyph.variation,
94
            extra_data: (),
95
        }
96
    }
97
}
98

            
99
pub fn gsub_apply_arabic(
100
    gsub_cache: &LayoutCache<GSUB>,
101
    gsub_table: &LayoutTable<GSUB>,
102
    gdef_table: Option<&GDEFTable>,
103
    script_tag: u32,
104
    lang_tag: Option<u32>,
105
    feature_variations: Option<&FeatureTableSubstitution<'_>>,
106
    extra_features: FeatureMask,
107
    raw_glyphs: &mut Vec<RawGlyph<()>>,
108
    max_glyphs: usize,
109
) -> Result<(), ShapingError> {
110
    match gsub_table.find_script(script_tag)? {
111
        Some(s) => {
112
            if s.find_langsys_or_default(lang_tag)?.is_none() {
113
                return Ok(());
114
            }
115
        }
116
        None => return Ok(()),
117
    }
118

            
119
    let arabic_glyphs = &mut raw_glyphs.iter().map(ArabicGlyph::from).collect();
120

            
121
    // 1. Compound character composition and decomposition
122

            
123
    apply_lookups(
124
        Feature::CCMP.mask(),
125
        gsub_cache,
126
        gsub_table,
127
        gdef_table,
128
        script_tag,
129
        lang_tag,
130
        feature_variations,
131
        arabic_glyphs,
132
        max_glyphs,
133
        |_, _| true,
134
    )?;
135

            
136
    // 2. Computing letter joining states
137

            
138
    {
139
        let mut previous_i = arabic_glyphs
140
            .iter()
141
            .position(|g| !g.is_transparent())
142
            .unwrap_or(0);
143

            
144
        for i in (previous_i + 1)..arabic_glyphs.len() {
145
            if arabic_glyphs[i].is_transparent() {
146
                continue;
147
            }
148

            
149
            if arabic_glyphs[previous_i].is_left_joining() && arabic_glyphs[i].is_right_joining() {
150
                arabic_glyphs[i].set_feature_tag(tag::FINA);
151

            
152
                match arabic_glyphs[previous_i].feature_tag() {
153
                    tag::ISOL => arabic_glyphs[previous_i].set_feature_tag(tag::INIT),
154
                    tag::FINA => arabic_glyphs[previous_i].set_feature_tag(tag::MEDI),
155
                    _ => {}
156
                }
157
            }
158

            
159
            previous_i = i;
160
        }
161
    }
162

            
163
    // 3. Applying the stch feature
164
    //
165
    // TODO hold off for future generalised solution (including the Syriac Abbreviation Mark)
166

            
167
    // 4. Applying the language-form substitution features from GSUB
168

            
169
    const LANGUAGE_FEATURES: &[(Feature, bool)] = &[
170
        (Feature::LOCL, true),
171
        (Feature::ISOL, false),
172
        (Feature::FINA, false),
173
        (Feature::MEDI, false),
174
        (Feature::INIT, false),
175
        (Feature::RLIG, true),
176
        (Feature::RCLT, true),
177
        (Feature::CALT, true),
178
    ];
179

            
180
    for &(feature, is_global) in LANGUAGE_FEATURES {
181
        apply_lookups(
182
            feature.mask(),
183
            gsub_cache,
184
            gsub_table,
185
            gdef_table,
186
            script_tag,
187
            lang_tag,
188
            feature_variations,
189
            arabic_glyphs,
190
            max_glyphs,
191
            |g, feature_tag| is_global || g.feature_tag() == feature_tag,
192
        )?;
193
    }
194

            
195
    // 5. Applying the typographic-form substitution features from GSUB
196

            
197
    let typographic_features = Feature::LIGA | Feature::MSET;
198

            
199
    apply_lookups(
200
        typographic_features | extra_features,
201
        gsub_cache,
202
        gsub_table,
203
        gdef_table,
204
        script_tag,
205
        lang_tag,
206
        feature_variations,
207
        arabic_glyphs,
208
        max_glyphs,
209
        |_, _| true,
210
    )?;
211

            
212
    // 6. Mark reordering
213
    //
214
    // Handled in the text preprocessing stage.
215

            
216
    *raw_glyphs = arabic_glyphs.iter().map(RawGlyph::from).collect();
217

            
218
    Ok(())
219
}
220

            
221
fn apply_lookups(
222
    feature_mask: FeatureMask,
223
    gsub_cache: &LayoutCache<GSUB>,
224
    gsub_table: &LayoutTable<GSUB>,
225
    gdef_table: Option<&GDEFTable>,
226
    script_tag: u32,
227
    lang_tag: Option<u32>,
228
    feature_variations: Option<&FeatureTableSubstitution<'_>>,
229
    arabic_glyphs: &mut Vec<ArabicGlyph>,
230
    max_glyphs: usize,
231
    pred: impl Fn(&ArabicGlyph, u32) -> bool + Copy,
232
) -> Result<(), ParseError> {
233
    let index = gsub::get_lookups_cache_index(
234
        gsub_cache,
235
        script_tag,
236
        lang_tag,
237
        feature_variations,
238
        feature_mask,
239
    )?;
240
    let lookups = &gsub_cache.cached_lookups.lock().unwrap()[index];
241

            
242
    for &(lookup_index, feature_tag) in lookups {
243
        gsub::gsub_apply_lookup(
244
            gsub_cache,
245
            gsub_table,
246
            gdef_table,
247
            lookup_index,
248
            feature_tag,
249
            None,
250
            arabic_glyphs,
251
            max_glyphs,
252
            0,
253
            arabic_glyphs.len(),
254
            |g| pred(g, feature_tag),
255
        )?;
256
    }
257

            
258
    Ok(())
259
}
260

            
261
/// Reorder Arabic marks per AMTRA. See: <https://www.unicode.org/reports/tr53/>.
262
pub(super) fn reorder_marks(cs: &mut [char]) {
263
    sort_by_modified_combining_class(cs);
264

            
265
    for css in
266
        cs.split_mut(|&c| modified_combining_class(c) == ModifiedCombiningClass::NotReordered)
267
    {
268
        reorder_marks_shadda(css);
269
        reorder_marks_other_combining(css, ModifiedCombiningClass::Above);
270
        reorder_marks_other_combining(css, ModifiedCombiningClass::Below);
271
    }
272
}
273

            
274
fn reorder_marks_shadda(cs: &mut [char]) {
275
    use std::cmp::Ordering;
276

            
277
    // 2a. Move any Shadda characters to the beginning of S, where S is a max
278
    // length substring of non-starter characters.
279
    fn comparator(c1: &char, _c2: &char) -> Ordering {
280
        if modified_combining_class(*c1) == ModifiedCombiningClass::CCC33 {
281
            Ordering::Less
282
        } else {
283
            Ordering::Equal
284
        }
285
    }
286
    cs.sort_by(comparator)
287
}
288

            
289
fn reorder_marks_other_combining(cs: &mut [char], mcc: ModifiedCombiningClass) {
290
    debug_assert!(mcc == ModifiedCombiningClass::Below || mcc == ModifiedCombiningClass::Above);
291

            
292
    // Get the start index of a possible sequence of characters with canonical
293
    // combining class equal to `mcc`. (Assumes that `glyphs` is normalised to
294
    // NFD.)
295
    let first = cs.iter().position(|&c| modified_combining_class(c) == mcc);
296

            
297
    if let Some(first) = first {
298
        // 2b/2c. If the sequence of characters _begins_ with any MCM characters,
299
        // move the sequence of such characters to the beginning of S.
300
        let count = cs[first..]
301
            .iter()
302
            .take_while(|&&c| is_modifier_combining_mark(c))
303
            .count();
304
        cs[..(first + count)].rotate_right(count);
305
    }
306
}
307

            
308
fn is_modifier_combining_mark(ch: char) -> bool {
309
    // https://www.unicode.org/reports/tr53/tr53-6.html#MCM
310
    match ch {
311
        | '\u{0654}' // ARABIC HAMZA ABOVE
312
        | '\u{0655}' // ARABIC HAMZA BELOW
313
        | '\u{0658}' // ARABIC MARK NOON GHUNNA
314
        | '\u{06DC}' // ARABIC SMALL HIGH SEEN
315
        | '\u{06E3}' // ARABIC SMALL LOW SEEN
316
        | '\u{06E7}' // ARABIC SMALL HIGH YEH
317
        | '\u{06E8}' // ARABIC SMALL HIGH NOON
318
        | '\u{08CA}' // ARABIC SMALL HIGH FARSI YEH
319
        | '\u{08CB}' // ARABIC SMALL HIGH YEH BARREE WITH TWO DOTS BELOW
320
        | '\u{08CD}' // ARABIC SMALL HIGH ZAH
321
        | '\u{08CE}' // ARABIC LARGE ROUND DOT ABOVE
322
        | '\u{08CF}' // ARABIC LARGE ROUND DOT BELOW
323
        | '\u{08D3}' // ARABIC SMALL LOW WAW
324
        | '\u{08F3}' => true, // ARABIC SMALL HIGH WAW
325
        _ => false,
326
    }
327
}
328

            
329
#[cfg(test)]
330
mod tests {
331
    use super::*;
332

            
333
    // https://www.unicode.org/reports/tr53/#Demonstrating_AMTRA.
334
    mod reorder_marks {
335
        use super::*;
336

            
337
        #[test]
338
        fn test_artificial() {
339
            let cs = vec![
340
                '\u{0618}', '\u{0619}', '\u{064E}', '\u{064F}', '\u{0654}', '\u{0658}', '\u{0653}',
341
                '\u{0654}', '\u{0651}', '\u{0656}', '\u{0651}', '\u{065C}', '\u{0655}', '\u{0650}',
342
            ];
343
            let cs_exp = vec![
344
                '\u{0654}', '\u{0658}', '\u{0651}', '\u{0651}', '\u{0618}', '\u{064E}', '\u{0619}',
345
                '\u{064F}', '\u{0650}', '\u{0656}', '\u{065C}', '\u{0655}', '\u{0653}', '\u{0654}',
346
            ];
347
            test_reorder_marks(&cs, &cs_exp);
348
        }
349

            
350
        // Variant of `test_artificial` where U+0656 is replaced with U+0655
351
        // to test the reordering of MCM characters for the ccc = 220 group.
352
        #[test]
353
        fn test_artificial_custom() {
354
            let cs = vec![
355
                '\u{0618}', '\u{0619}', '\u{064E}', '\u{064F}', '\u{0654}', '\u{0658}', '\u{0653}',
356
                '\u{0654}', '\u{0651}', '\u{0655}', '\u{0651}', '\u{065C}', '\u{0655}', '\u{0650}',
357
            ];
358
            let cs_exp = vec![
359
                '\u{0655}', '\u{0654}', '\u{0658}', '\u{0651}', '\u{0651}', '\u{0618}', '\u{064E}',
360
                '\u{0619}', '\u{064F}', '\u{0650}', '\u{065C}', '\u{0655}', '\u{0653}', '\u{0654}',
361
            ];
362
            test_reorder_marks(&cs, &cs_exp);
363
        }
364

            
365
        #[test]
366
        fn test_example1() {
367
            let cs1 = vec!['\u{0627}', '\u{064F}', '\u{0654}'];
368
            let cs1_exp = vec!['\u{0627}', '\u{0654}', '\u{064F}'];
369
            test_reorder_marks(&cs1, &cs1_exp);
370

            
371
            let cs2 = vec!['\u{0627}', '\u{064F}', '\u{034F}', '\u{0654}'];
372
            test_reorder_marks(&cs2, &cs2);
373

            
374
            let cs3 = vec!['\u{0649}', '\u{0650}', '\u{0655}'];
375
            let cs3_exp = vec!['\u{0649}', '\u{0655}', '\u{0650}'];
376
            test_reorder_marks(&cs3, &cs3_exp);
377

            
378
            let cs4 = vec!['\u{0649}', '\u{0650}', '\u{034F}', '\u{0655}'];
379
            test_reorder_marks(&cs4, &cs4);
380
        }
381

            
382
        #[test]
383
        fn test_example2a() {
384
            let cs = vec!['\u{0635}', '\u{06DC}', '\u{0652}'];
385
            test_reorder_marks(&cs, &cs);
386
        }
387

            
388
        #[test]
389
        fn test_example2b() {
390
            let cs1 = vec!['\u{0647}', '\u{0652}', '\u{06DC}'];
391
            let cs1_exp = vec!['\u{0647}', '\u{06DC}', '\u{0652}'];
392
            test_reorder_marks(&cs1, &cs1_exp);
393

            
394
            let cs2 = vec!['\u{0647}', '\u{0652}', '\u{034F}', '\u{06DC}'];
395
            test_reorder_marks(&cs2, &cs2);
396
        }
397

            
398
        #[test]
399
        fn test_example3() {
400
            let cs1 = vec!['\u{0640}', '\u{0650}', '\u{0651}', '\u{06E7}'];
401
            // The expected output in https://www.unicode.org/reports/tr53/#Example3
402
            //
403
            // [U+0640, U+0650, U+06E7, U+0651]
404
            //
405
            // is incorrect, in that it fails to account for U+0651 Shadda moving to
406
            // the front of U+0650 Kasra, per step 2a of AMTRA.
407
            //
408
            // U+06E7 Small High Yeh should then move to the front of Shadda per step
409
            // 2b, resulting in:
410
            let cs1_exp = vec!['\u{0640}', '\u{06E7}', '\u{0651}', '\u{0650}'];
411
            test_reorder_marks(&cs1, &cs1_exp);
412

            
413
            let cs2 = vec!['\u{0640}', '\u{0650}', '\u{0651}', '\u{034F}', '\u{06E7}'];
414
            // As above, Shadda should move to the front of Kasra, so the expected
415
            // output in https://www.unicode.org/reports/tr53/#Example3
416
            //
417
            // [U+0640, U+0650, U+0651, U+034F, U+06E7]
418
            //
419
            // (i.e. no changes) is also incorrect.
420
            let cs2_exp = vec!['\u{0640}', '\u{0651}', '\u{0650}', '\u{034F}', '\u{06E7}'];
421
            test_reorder_marks(&cs2, &cs2_exp);
422
        }
423

            
424
        #[test]
425
        fn test_example4a() {
426
            let cs = vec!['\u{0640}', '\u{0652}', '\u{034F}', '\u{06E8}'];
427
            test_reorder_marks(&cs, &cs);
428
        }
429

            
430
        #[test]
431
        fn test_example4b() {
432
            let cs1 = vec!['\u{06C6}', '\u{064F}', '\u{06E8}'];
433
            let cs1_exp = vec!['\u{06C6}', '\u{06E8}', '\u{064F}'];
434
            test_reorder_marks(&cs1, &cs1_exp);
435

            
436
            let cs2 = vec!['\u{06C6}', '\u{064F}', '\u{034F}', '\u{06E8}'];
437
            test_reorder_marks(&cs2, &cs2);
438
        }
439

            
440
        fn test_reorder_marks(cs: &Vec<char>, cs_exp: &Vec<char>) {
441
            let mut cs_act = cs.clone();
442
            reorder_marks(&mut cs_act);
443
            assert_eq!(cs_exp, &cs_act);
444
        }
445
    }
446
}