allsorts_subset_browser/scripts/
syriac.rs

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
6use unicode_joining_type::{get_joining_group, get_joining_type, JoiningGroup, JoiningType};
7
8use crate::error::{ParseError, ShapingError};
9use crate::gsub::{self, FeatureMask, GlyphData, GlyphOrigin, RawGlyph};
10use crate::layout::{FeatureTableSubstitution, GDEFTable, LayoutCache, LayoutTable, GSUB};
11use crate::tag;
12
13#[derive(Clone)]
14struct SyriacData {
15    joining_group: JoiningGroup,
16    joining_type: JoiningType,
17    feature_tag: u32,
18}
19
20impl 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`.
28type SyriacGlyph = RawGlyph<SyriacData>;
29
30impl 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
68impl 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
102impl 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
116pub 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    raw_glyphs: &mut Vec<RawGlyph<()>>,
124) -> Result<(), ShapingError> {
125    match gsub_table.find_script(script_tag)? {
126        Some(s) => {
127            if s.find_langsys_or_default(lang_tag)?.is_none() {
128                return Ok(());
129            }
130        }
131        None => return Ok(()),
132    }
133
134    let syriac_glyphs: &mut Vec<SyriacGlyph> =
135        &mut raw_glyphs.iter().map(SyriacGlyph::from).collect();
136
137    // 1. Compound character composition and decomposition
138
139    apply_lookups(
140        FeatureMask::CCMP,
141        gsub_cache,
142        gsub_table,
143        gdef_table,
144        script_tag,
145        lang_tag,
146        feature_variations,
147        syriac_glyphs,
148        |_, _| true,
149    )?;
150
151    // 2. Computing letter joining states
152
153    {
154        let mut previous_i = syriac_glyphs
155            .iter()
156            .position(|g| !g.is_transparent())
157            .unwrap_or(0);
158
159        for i in (previous_i + 1)..syriac_glyphs.len() {
160            if syriac_glyphs[i].is_transparent() {
161                continue;
162            }
163
164            if syriac_glyphs[previous_i].is_left_joining() && syriac_glyphs[i].is_right_joining() {
165                if syriac_glyphs[i].is_alaph() {
166                    syriac_glyphs[i].set_feature_tag(tag::MED2)
167                } else {
168                    syriac_glyphs[i].set_feature_tag(tag::FINA)
169                };
170
171                match syriac_glyphs[previous_i].feature_tag() {
172                    tag::ISOL => syriac_glyphs[previous_i].set_feature_tag(tag::INIT),
173                    tag::FINA => syriac_glyphs[previous_i].set_feature_tag(tag::MEDI),
174                    _ => {}
175                }
176            }
177
178            previous_i = i;
179        }
180
181        let last_i = syriac_glyphs
182            .iter()
183            .rposition(|g| !(g.is_transparent() || g.is_non_joining()))
184            .unwrap_or(0);
185
186        if last_i != 0 && syriac_glyphs[last_i].is_alaph() {
187            let previous_i = last_i - 1;
188
189            if syriac_glyphs[previous_i].is_left_joining() {
190                syriac_glyphs[last_i].set_feature_tag(tag::FINA)
191            } else if syriac_glyphs[previous_i].is_dalath_rish() {
192                syriac_glyphs[last_i].set_feature_tag(tag::FIN3)
193            } else {
194                syriac_glyphs[last_i].set_feature_tag(tag::FIN2)
195            }
196        }
197    }
198
199    // 3. Applying the stch feature
200    //
201    // TODO hold off for future generalised solution (including Kashidas)
202
203    // 4. Applying the language-form substitution features from GSUB
204
205    const LANGUAGE_FEATURES: &[(FeatureMask, bool)] = &[
206        (FeatureMask::LOCL, true),
207        (FeatureMask::ISOL, false),
208        (FeatureMask::FINA, false),
209        (FeatureMask::FIN2, false),
210        (FeatureMask::FIN3, false),
211        (FeatureMask::MEDI, false),
212        (FeatureMask::MED2, false),
213        (FeatureMask::INIT, false),
214        (FeatureMask::RLIG, true),
215        (FeatureMask::CALT, true),
216    ];
217
218    for &(feature_mask, is_global) in LANGUAGE_FEATURES {
219        apply_lookups(
220            feature_mask,
221            gsub_cache,
222            gsub_table,
223            gdef_table,
224            script_tag,
225            lang_tag,
226            feature_variations,
227            syriac_glyphs,
228            |g, feature_tag| is_global || g.feature_tag() == feature_tag,
229        )?;
230    }
231
232    // 5. Applying the typographic-form substitution features from GSUB to all glyphs
233    //
234    // Note that we skip `GSUB`'s `DLIG` feature as it should be off by default
235
236    apply_lookups(
237        FeatureMask::LIGA,
238        gsub_cache,
239        gsub_table,
240        gdef_table,
241        script_tag,
242        lang_tag,
243        feature_variations,
244        syriac_glyphs,
245        |_, _| true,
246    )?;
247
248    // 6. Mark reordering
249    //
250    // TODO hold off for future Unicode normalisation changes
251
252    *raw_glyphs = syriac_glyphs.iter().map(RawGlyph::from).collect();
253
254    Ok(())
255}
256
257fn apply_lookups(
258    feature_mask: FeatureMask,
259    gsub_cache: &LayoutCache<GSUB>,
260    gsub_table: &LayoutTable<GSUB>,
261    gdef_table: Option<&GDEFTable>,
262    script_tag: u32,
263    lang_tag: Option<u32>,
264    feature_variations: Option<&FeatureTableSubstitution<'_>>,
265    syriac_glyphs: &mut Vec<RawGlyph<SyriacData>>,
266    pred: impl Fn(&RawGlyph<SyriacData>, u32) -> bool + Copy,
267) -> Result<(), ParseError> {
268    let index = gsub::get_lookups_cache_index(
269        gsub_cache,
270        script_tag,
271        lang_tag,
272        feature_variations,
273        feature_mask,
274    )?;
275    let lookups = &gsub_cache.cached_lookups.borrow()[index];
276
277    for &(lookup_index, feature_tag) in lookups {
278        gsub::gsub_apply_lookup(
279            gsub_cache,
280            gsub_table,
281            gdef_table,
282            lookup_index,
283            feature_tag,
284            None,
285            syriac_glyphs,
286            0,
287            syriac_glyphs.len(),
288            |g| pred(g, feature_tag),
289        )?;
290    }
291
292    Ok(())
293}