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

            
6
use unicode_joining_type::{get_joining_group, get_joining_type, JoiningGroup, 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

            
13
#[derive(Clone)]
14
struct SyriacData {
15
    joining_group: JoiningGroup,
16
    joining_type: JoiningType,
17
    feature_tag: u32,
18
}
19

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

            
27
// Syriac glyphs are represented as `RawGlyph` structs with `SyriacData` for its `extra_data`.
28
type SyriacGlyph = RawGlyph<SyriacData>;
29

            
30
impl SyriacGlyph {
31
    fn is_alaph(&self) -> bool {
32
        self.extra_data.joining_group == JoiningGroup::Alaph
33
    }
34

            
35
    fn is_dalath_rish(&self) -> bool {
36
        self.extra_data.joining_group == JoiningGroup::DalathRish
37
    }
38

            
39
    fn is_transparent(&self) -> bool {
40
        self.extra_data.joining_type == JoiningType::Transparent || self.multi_subst_dup()
41
    }
42

            
43
    fn is_non_joining(&self) -> bool {
44
        self.extra_data.joining_type == JoiningType::NonJoining
45
    }
46

            
47
    fn is_left_joining(&self) -> bool {
48
        self.extra_data.joining_type == JoiningType::LeftJoining
49
            || self.extra_data.joining_type == JoiningType::DualJoining
50
            || self.extra_data.joining_type == JoiningType::JoinCausing
51
    }
52

            
53
    fn is_right_joining(&self) -> bool {
54
        self.extra_data.joining_type == JoiningType::RightJoining
55
            || self.extra_data.joining_type == JoiningType::DualJoining
56
            || self.extra_data.joining_type == JoiningType::JoinCausing
57
    }
58

            
59
    fn feature_tag(&self) -> u32 {
60
        self.extra_data.feature_tag
61
    }
62

            
63
    fn set_feature_tag(&mut self, feature_tag: u32) {
64
        self.extra_data.feature_tag = feature_tag
65
    }
66
}
67

            
68
impl From<&RawGlyph<()>> for SyriacGlyph {
69
    fn from(raw_glyph: &RawGlyph<()>) -> SyriacGlyph {
70
        // Since there's no `Char` to work out the `SyriacGlyph`s joining type when the glyph's
71
        // `glyph_origin` is `GlyphOrigin::Direct`, we fallback to `JoiningType::NonJoining` as
72
        // the safest approach
73
        let joining_type = match raw_glyph.glyph_origin {
74
            GlyphOrigin::Char(c) => get_joining_type(c),
75
            GlyphOrigin::Direct => JoiningType::NonJoining,
76
        };
77

            
78
        // As above, we'll fallback onto `JoiningType::NoJoiningGroup`
79
        let joining_group = match raw_glyph.glyph_origin {
80
            GlyphOrigin::Char(c) => get_joining_group(c),
81
            GlyphOrigin::Direct => JoiningGroup::NoJoiningGroup,
82
        };
83

            
84
        SyriacGlyph {
85
            unicodes: raw_glyph.unicodes.clone(),
86
            glyph_index: raw_glyph.glyph_index,
87
            liga_component_pos: raw_glyph.liga_component_pos,
88
            glyph_origin: raw_glyph.glyph_origin,
89
            flags: raw_glyph.flags,
90
            variation: raw_glyph.variation,
91
            extra_data: SyriacData {
92
                joining_group,
93
                joining_type,
94
                // For convenience, we loosely follow the spec (`2. Computing letter joining
95
                // states`) here by initialising all `SyriacGlyph`s to `tag::ISOL`
96
                feature_tag: tag::ISOL,
97
            },
98
        }
99
    }
100
}
101

            
102
impl From<&SyriacGlyph> for RawGlyph<()> {
103
    fn from(syriac_glyph: &SyriacGlyph) -> RawGlyph<()> {
104
        RawGlyph {
105
            unicodes: syriac_glyph.unicodes.clone(),
106
            glyph_index: syriac_glyph.glyph_index,
107
            liga_component_pos: syriac_glyph.liga_component_pos,
108
            glyph_origin: syriac_glyph.glyph_origin,
109
            flags: syriac_glyph.flags,
110
            variation: syriac_glyph.variation,
111
            extra_data: (),
112
        }
113
    }
114
}
115

            
116
pub fn gsub_apply_syriac(
117
    gsub_cache: &LayoutCache<GSUB>,
118
    gsub_table: &LayoutTable<GSUB>,
119
    gdef_table: Option<&GDEFTable>,
120
    script_tag: u32,
121
    lang_tag: Option<u32>,
122
    feature_variations: Option<&FeatureTableSubstitution<'_>>,
123
    extra_features: FeatureMask,
124
    raw_glyphs: &mut Vec<RawGlyph<()>>,
125
    max_glyphs: usize,
126
) -> Result<(), ShapingError> {
127
    match gsub_table.find_script(script_tag)? {
128
        Some(s) => {
129
            if s.find_langsys_or_default(lang_tag)?.is_none() {
130
                return Ok(());
131
            }
132
        }
133
        None => return Ok(()),
134
    }
135

            
136
    let syriac_glyphs: &mut Vec<SyriacGlyph> =
137
        &mut raw_glyphs.iter().map(SyriacGlyph::from).collect();
138

            
139
    // 1. Compound character composition and decomposition
140

            
141
    apply_lookups(
142
        Feature::CCMP.mask(),
143
        gsub_cache,
144
        gsub_table,
145
        gdef_table,
146
        script_tag,
147
        lang_tag,
148
        feature_variations,
149
        syriac_glyphs,
150
        max_glyphs,
151
        |_, _| true,
152
    )?;
153

            
154
    // 2. Computing letter joining states
155

            
156
    {
157
        let mut previous_i = syriac_glyphs
158
            .iter()
159
            .position(|g| !g.is_transparent())
160
            .unwrap_or(0);
161

            
162
        for i in (previous_i + 1)..syriac_glyphs.len() {
163
            if syriac_glyphs[i].is_transparent() {
164
                continue;
165
            }
166

            
167
            if syriac_glyphs[previous_i].is_left_joining() && syriac_glyphs[i].is_right_joining() {
168
                if syriac_glyphs[i].is_alaph() {
169
                    syriac_glyphs[i].set_feature_tag(tag::MED2)
170
                } else {
171
                    syriac_glyphs[i].set_feature_tag(tag::FINA)
172
                };
173

            
174
                match syriac_glyphs[previous_i].feature_tag() {
175
                    tag::ISOL => syriac_glyphs[previous_i].set_feature_tag(tag::INIT),
176
                    tag::FINA => syriac_glyphs[previous_i].set_feature_tag(tag::MEDI),
177
                    _ => {}
178
                }
179
            }
180

            
181
            previous_i = i;
182
        }
183

            
184
        let last_i = syriac_glyphs
185
            .iter()
186
            .rposition(|g| !(g.is_transparent() || g.is_non_joining()))
187
            .unwrap_or(0);
188

            
189
        if last_i != 0 && syriac_glyphs[last_i].is_alaph() {
190
            let previous_i = last_i - 1;
191

            
192
            if syriac_glyphs[previous_i].is_left_joining() {
193
                syriac_glyphs[last_i].set_feature_tag(tag::FINA)
194
            } else if syriac_glyphs[previous_i].is_dalath_rish() {
195
                syriac_glyphs[last_i].set_feature_tag(tag::FIN3)
196
            } else {
197
                syriac_glyphs[last_i].set_feature_tag(tag::FIN2)
198
            }
199
        }
200
    }
201

            
202
    // 3. Applying the stch feature
203
    //
204
    // TODO hold off for future generalised solution (including Kashidas)
205

            
206
    // 4. Applying the language-form substitution features from GSUB
207

            
208
    const LANGUAGE_FEATURES: &[(Feature, bool)] = &[
209
        (Feature::LOCL, true),
210
        (Feature::ISOL, false),
211
        (Feature::FINA, false),
212
        (Feature::FIN2, false),
213
        (Feature::FIN3, false),
214
        (Feature::MEDI, false),
215
        (Feature::MED2, false),
216
        (Feature::INIT, false),
217
        (Feature::RLIG, true),
218
        (Feature::CALT, true),
219
    ];
220

            
221
    for &(feature, is_global) in LANGUAGE_FEATURES {
222
        apply_lookups(
223
            feature.mask(),
224
            gsub_cache,
225
            gsub_table,
226
            gdef_table,
227
            script_tag,
228
            lang_tag,
229
            feature_variations,
230
            syriac_glyphs,
231
            max_glyphs,
232
            |g, feature_tag| is_global || g.feature_tag() == feature_tag,
233
        )?;
234
    }
235

            
236
    // 5. Applying the typographic-form substitution features from GSUB to all glyphs
237

            
238
    let typographic_features: FeatureMask = Feature::LIGA.mask();
239

            
240
    apply_lookups(
241
        typographic_features | extra_features,
242
        gsub_cache,
243
        gsub_table,
244
        gdef_table,
245
        script_tag,
246
        lang_tag,
247
        feature_variations,
248
        syriac_glyphs,
249
        max_glyphs,
250
        |_, _| true,
251
    )?;
252

            
253
    // 6. Mark reordering
254
    //
255
    // TODO hold off for future Unicode normalisation changes
256

            
257
    *raw_glyphs = syriac_glyphs.iter().map(RawGlyph::from).collect();
258

            
259
    Ok(())
260
}
261

            
262
fn apply_lookups(
263
    feature_mask: FeatureMask,
264
    gsub_cache: &LayoutCache<GSUB>,
265
    gsub_table: &LayoutTable<GSUB>,
266
    gdef_table: Option<&GDEFTable>,
267
    script_tag: u32,
268
    lang_tag: Option<u32>,
269
    feature_variations: Option<&FeatureTableSubstitution<'_>>,
270
    syriac_glyphs: &mut Vec<RawGlyph<SyriacData>>,
271
    max_glyphs: usize,
272
    pred: impl Fn(&RawGlyph<SyriacData>, u32) -> bool + Copy,
273
) -> Result<(), ParseError> {
274
    let index = gsub::get_lookups_cache_index(
275
        gsub_cache,
276
        script_tag,
277
        lang_tag,
278
        feature_variations,
279
        feature_mask,
280
    )?;
281
    let lookups = &gsub_cache.cached_lookups.lock().unwrap()[index];
282

            
283
    for &(lookup_index, feature_tag) in lookups {
284
        gsub::gsub_apply_lookup(
285
            gsub_cache,
286
            gsub_table,
287
            gdef_table,
288
            lookup_index,
289
            feature_tag,
290
            None,
291
            syriac_glyphs,
292
            max_glyphs,
293
            0,
294
            syriac_glyphs.len(),
295
            |g| pred(g, feature_tag),
296
        )?;
297
    }
298

            
299
    Ok(())
300
}