Skip to main content

read_fonts/tables/gsub/
closure.rs

1//! Computing the closure over a set of glyphs
2//!
3//! This means taking a set of glyphs and updating it to include any other glyphs
4//! reachable from those glyphs via substitution, recursively.
5use font_types::GlyphId;
6
7use crate::{
8    collections::IntSet,
9    tables::layout::{ExtensionLookup, Subtables},
10    FontRead, ReadError, Tag,
11};
12
13use super::{
14    AlternateSubstFormat1, ChainedSequenceContext, Gsub, Ligature, LigatureSet,
15    LigatureSubstFormat1, MultipleSubstFormat1, ReverseChainSingleSubstFormat1, SequenceContext,
16    SingleSubst, SingleSubstFormat1, SingleSubstFormat2, SubstitutionLookup,
17    SubstitutionLookupList, SubstitutionSubtables,
18};
19
20#[cfg(feature = "std")]
21use crate::tables::layout::{
22    ContextFormat1, ContextFormat2, ContextFormat3, Intersect, LayoutLookupList, LookupClosure,
23    LookupClosureCtx,
24};
25
26// we put ClosureCtx in its own module to enforce visibility rules;
27// specifically we don't want cur_glyphs to be reachable directly
28mod ctx {
29    use std::collections::HashMap;
30    use types::GlyphId;
31
32    use crate::{
33        collections::IntSet,
34        tables::gsub::{SubstitutionLookup, SubstitutionLookupList},
35    };
36
37    use super::GlyphClosure as _;
38    use super::ReadError;
39
40    #[cfg(feature = "std")]
41    use crate::tables::layout::{MAX_LOOKUP_VISIT_COUNT, MAX_NESTING_LEVEL};
42
43    pub(super) struct ClosureCtx<'a> {
44        /// the current closure glyphs. This is updated as we go.
45        glyphs: &'a mut IntSet<GlyphId>,
46        active_glyphs_stack: Vec<IntSet<GlyphId>>,
47        output: IntSet<GlyphId>,
48        lookup_count: u16,
49        nesting_level_left: u8,
50        done_lookups_glyphs: HashMap<u16, (u64, IntSet<GlyphId>)>,
51    }
52
53    impl<'a> ClosureCtx<'a> {
54        pub(super) fn new(glyphs: &'a mut IntSet<GlyphId>) -> Self {
55            Self {
56                glyphs,
57                active_glyphs_stack: Vec::new(),
58                output: IntSet::empty(),
59                lookup_count: 0,
60                nesting_level_left: MAX_NESTING_LEVEL,
61                done_lookups_glyphs: Default::default(),
62            }
63        }
64
65        pub(super) fn lookup_limit_exceed(&self) -> bool {
66            self.lookup_count > MAX_LOOKUP_VISIT_COUNT
67        }
68
69        pub(super) fn parent_active_glyphs(&self) -> &IntSet<GlyphId> {
70            if self.active_glyphs_stack.is_empty() {
71                return &*self.glyphs;
72            }
73
74            self.active_glyphs_stack.last().unwrap()
75        }
76
77        pub(super) fn push_cur_active_glyphs(&mut self, glyphs: IntSet<GlyphId>) {
78            self.active_glyphs_stack.push(glyphs)
79        }
80
81        pub(super) fn pop_cur_done_glyphs(&mut self) {
82            self.active_glyphs_stack.pop();
83        }
84
85        #[allow(clippy::too_many_arguments)]
86        pub(super) fn recurse(
87            &mut self,
88            lookup_list: &SubstitutionLookupList,
89            lookup: &SubstitutionLookup,
90            lookup_index: u16,
91            glyphs: IntSet<GlyphId>,
92            seen_seq_indices: &mut IntSet<u16>,
93            seq_idx: u16,
94            end_idx: u16,
95        ) -> Result<(), ReadError> {
96            if self.nesting_level_left == 0 {
97                return Ok(());
98            }
99
100            self.nesting_level_left -= 1;
101            self.push_cur_active_glyphs(glyphs);
102
103            if !self.should_visit_lookup(lookup_index) {
104                self.nesting_level_left += 1;
105                self.pop_cur_done_glyphs();
106                return Ok(());
107            }
108
109            if lookup.may_have_non_1to1()? {
110                seen_seq_indices.insert_range(seq_idx..=end_idx);
111            }
112            lookup
113                .subtables()?
114                .closure_glyphs(self, lookup_list, lookup_index)?;
115
116            self.nesting_level_left += 1;
117            self.pop_cur_done_glyphs();
118
119            Ok(())
120        }
121
122        pub(super) fn reset_lookup_visit_count(&mut self) {
123            self.lookup_count = 0;
124        }
125
126        pub(super) fn should_visit_lookup(&mut self, lookup_index: u16) -> bool {
127            if self.lookup_limit_exceed() {
128                return false;
129            }
130            self.lookup_count += 1;
131            !self.is_lookup_done(lookup_index)
132        }
133
134        // Return true if we have visited this lookup with current set of glyphs
135        pub(super) fn is_lookup_done(&mut self, lookup_index: u16) -> bool {
136            let mut cur_active_glyphs = IntSet::empty();
137            cur_active_glyphs.union(self.parent_active_glyphs());
138
139            let (count, covered) = self
140                .done_lookups_glyphs
141                .entry(lookup_index)
142                .or_insert((0, IntSet::empty()));
143
144            if *count != self.glyphs.len() {
145                *count = self.glyphs.len();
146                covered.clear();
147            }
148
149            //TODO: add IntSet::is_subset
150            if cur_active_glyphs.iter().all(|g| covered.contains(g)) {
151                return true;
152            }
153
154            covered.union(&cur_active_glyphs);
155            false
156        }
157
158        pub(super) fn glyphs(&self) -> &IntSet<GlyphId> {
159            self.glyphs
160        }
161
162        pub(super) fn add(&mut self, gid: GlyphId) {
163            self.output.insert(gid);
164        }
165
166        pub(super) fn add_glyphs(&mut self, iter: impl IntoIterator<Item = GlyphId>) {
167            self.output.extend(iter)
168        }
169
170        pub(super) fn flush(&mut self) {
171            self.glyphs.union(&self.output);
172            self.output.clear();
173            self.active_glyphs_stack.clear();
174        }
175    }
176}
177
178use ctx::ClosureCtx;
179
180/// A trait for tables which participate in closure
181trait GlyphClosure {
182    /// Update the set of glyphs with any glyphs reachable via substitution.
183    fn closure_glyphs(
184        &self,
185        ctx: &mut ClosureCtx,
186        lookup_list: &SubstitutionLookupList,
187        lookup_index: u16,
188    ) -> Result<(), ReadError>;
189
190    fn may_have_non_1to1(&self) -> Result<bool, ReadError> {
191        Ok(false)
192    }
193}
194
195const CLOSURE_MAX_STAGES: u8 = 12;
196impl Gsub<'_> {
197    /// Return the set of glyphs reachable from the input set via any substitution.
198    /// ref: <https://github.com/harfbuzz/harfbuzz/blob/8d517f7e43f648cb804c46c47ae8009330fe4a47/src/hb-ot-layout.cc#L1616>
199    pub fn closure_glyphs(
200        &self,
201        lookups: &IntSet<u16>,
202        glyphs: &mut IntSet<GlyphId>,
203    ) -> Result<(), ReadError> {
204        if self.lookup_list_offset().is_null() {
205            return Ok(());
206        }
207        let lookup_list = self.lookup_list()?;
208        let num_lookups = lookup_list.lookup_count();
209        let lookup_offsets = lookup_list.lookups();
210
211        let mut ctx = ClosureCtx::new(glyphs);
212        let mut iteration_count = 0;
213        let mut glyphs_length;
214        loop {
215            ctx.reset_lookup_visit_count();
216            glyphs_length = ctx.glyphs().len();
217
218            if lookups.is_inverted() {
219                for i in 0..num_lookups {
220                    if !lookups.contains(i) {
221                        continue;
222                    }
223                    let lookup = match lookup_offsets.get(i as usize) {
224                        Err(ReadError::NullOffset) => continue,
225                        other => other,
226                    }?;
227                    lookup.closure_glyphs(&mut ctx, &lookup_list, i)?;
228                    ctx.flush();
229                }
230            } else {
231                for i in lookups.iter() {
232                    let lookup = match lookup_offsets.get(i as usize) {
233                        Err(ReadError::NullOffset) | Err(ReadError::InvalidCollectionIndex(_)) => {
234                            continue
235                        }
236                        other => other,
237                    }?;
238                    lookup.closure_glyphs(&mut ctx, &lookup_list, i)?;
239                    ctx.flush();
240                }
241            }
242            if iteration_count > CLOSURE_MAX_STAGES || glyphs_length == ctx.glyphs().len() {
243                break;
244            }
245            iteration_count += 1;
246        }
247        Ok(())
248    }
249
250    /// Return a set of lookups referenced by the specified features
251    ///
252    /// Pass `&IntSet::all()` to get the lookups referenced by all features.
253    pub fn collect_lookups(&self, feature_indices: &IntSet<u16>) -> Result<IntSet<u16>, ReadError> {
254        if self.feature_list_offset().is_null() {
255            return Ok(IntSet::empty());
256        }
257        let feature_list = self.feature_list()?;
258        let mut lookup_indices = feature_list.collect_lookups(feature_indices)?;
259
260        if let Some(feature_variations) = self.feature_variations().transpose()? {
261            let subs_lookup_indices = feature_variations.collect_lookups(feature_indices)?;
262            lookup_indices.union(&subs_lookup_indices);
263        }
264        Ok(lookup_indices)
265    }
266
267    /// Return a set of all feature indices underneath the specified scripts, languages and features
268    pub fn collect_features(
269        &self,
270        scripts: &IntSet<Tag>,
271        languages: &IntSet<Tag>,
272        features: &IntSet<Tag>,
273    ) -> Result<IntSet<u16>, ReadError> {
274        if self.script_list_offset().is_null() || self.feature_list_offset().is_null() {
275            return Ok(IntSet::empty());
276        }
277        let feature_list = self.feature_list()?;
278        let script_list = self.script_list()?;
279        let head_ptr = self.offset_data().as_bytes().as_ptr() as usize;
280        script_list.collect_features(head_ptr, &feature_list, scripts, languages, features)
281    }
282
283    /// Update the set of lookup indices with all lookups reachable from specified glyph set and lookup_indices.
284    pub fn closure_lookups(
285        &self,
286        glyphs: &IntSet<GlyphId>,
287        lookup_indices: &mut IntSet<u16>,
288    ) -> Result<(), ReadError> {
289        if self.lookup_list_offset().is_null() {
290            return Ok(());
291        }
292        let lookup_list = self.lookup_list()?;
293        lookup_list.closure_lookups(glyphs, lookup_indices)
294    }
295}
296
297//ref: <https://github.com/harfbuzz/harfbuzz/blob/8d517f7e43f648cb804c46c47ae8009330fe4a47/src/OT/Layout/GSUB/SubstLookup.hh#L50>
298impl GlyphClosure for SubstitutionLookup<'_> {
299    fn closure_glyphs(
300        &self,
301        ctx: &mut ClosureCtx,
302        lookup_list: &SubstitutionLookupList,
303        lookup_index: u16,
304    ) -> Result<(), ReadError> {
305        if !ctx.should_visit_lookup(lookup_index) {
306            return Ok(());
307        }
308        self.subtables()?
309            .closure_glyphs(ctx, lookup_list, lookup_index)
310    }
311
312    fn may_have_non_1to1(&self) -> Result<bool, ReadError> {
313        self.subtables()?.may_have_non_1to1()
314    }
315}
316
317impl GlyphClosure for SubstitutionSubtables<'_> {
318    fn closure_glyphs(
319        &self,
320        ctx: &mut ClosureCtx,
321        lookup_list: &SubstitutionLookupList,
322        lookup_index: u16,
323    ) -> Result<(), ReadError> {
324        match self {
325            SubstitutionSubtables::Single(tables) => {
326                tables.closure_glyphs(ctx, lookup_list, lookup_index)
327            }
328            SubstitutionSubtables::Multiple(tables) => {
329                tables.closure_glyphs(ctx, lookup_list, lookup_index)
330            }
331            SubstitutionSubtables::Alternate(tables) => {
332                tables.closure_glyphs(ctx, lookup_list, lookup_index)
333            }
334            SubstitutionSubtables::Ligature(tables) => {
335                tables.closure_glyphs(ctx, lookup_list, lookup_index)
336            }
337            SubstitutionSubtables::Reverse(tables) => {
338                tables.closure_glyphs(ctx, lookup_list, lookup_index)
339            }
340            SubstitutionSubtables::Contextual(tables) => {
341                tables.closure_glyphs(ctx, lookup_list, lookup_index)
342            }
343            SubstitutionSubtables::ChainContextual(tables) => {
344                tables.closure_glyphs(ctx, lookup_list, lookup_index)
345            }
346            SubstitutionSubtables::EmptyExtension => Ok(()),
347        }
348    }
349
350    fn may_have_non_1to1(&self) -> Result<bool, ReadError> {
351        match self {
352            SubstitutionSubtables::Single(_) => Ok(false),
353            SubstitutionSubtables::Multiple(_) => Ok(true),
354            SubstitutionSubtables::Alternate(_) => Ok(false),
355            SubstitutionSubtables::Ligature(_) => Ok(true),
356            SubstitutionSubtables::Reverse(_) => Ok(false),
357            SubstitutionSubtables::Contextual(_) => Ok(true),
358            SubstitutionSubtables::ChainContextual(_) => Ok(true),
359            SubstitutionSubtables::EmptyExtension => Ok(false),
360        }
361    }
362}
363
364impl<'a, T: FontRead<'a> + GlyphClosure + 'a, Ext: ExtensionLookup<'a, T> + 'a> GlyphClosure
365    for Subtables<'a, T, Ext>
366{
367    fn closure_glyphs(
368        &self,
369        ctx: &mut ClosureCtx,
370        lookup_list: &SubstitutionLookupList,
371        lookup_index: u16,
372    ) -> Result<(), ReadError> {
373        for t in self.iter().filter_map(|table| match table {
374            Err(ReadError::NullOffset) => None,
375            other => Some(other),
376        }) {
377            t?.closure_glyphs(ctx, lookup_list, lookup_index)?;
378        }
379        Ok(())
380    }
381}
382
383impl GlyphClosure for SingleSubst<'_> {
384    fn closure_glyphs(
385        &self,
386        ctx: &mut ClosureCtx,
387        lookup_list: &SubstitutionLookupList,
388        lookup_index: u16,
389    ) -> Result<(), ReadError> {
390        match self {
391            SingleSubst::Format1(t) => t.closure_glyphs(ctx, lookup_list, lookup_index),
392            SingleSubst::Format2(t) => t.closure_glyphs(ctx, lookup_list, lookup_index),
393        }
394    }
395}
396
397// ref: <https://github.com/harfbuzz/harfbuzz/blob/8d517f7e43f648cb804c46c47ae8009330fe4a47/src/OT/Layout/GSUB/SingleSubstFormat1.hh#L48>
398impl GlyphClosure for SingleSubstFormat1<'_> {
399    fn closure_glyphs(
400        &self,
401        ctx: &mut ClosureCtx,
402        _lookup_list: &SubstitutionLookupList,
403        _lookup_index: u16,
404    ) -> Result<(), ReadError> {
405        if self.coverage_offset().is_null() {
406            return Ok(());
407        }
408        let coverage = self.coverage()?;
409        let num_glyphs = coverage.population();
410        let mask = u16::MAX;
411        // ref: <https://github.com/harfbuzz/harfbuzz/blob/fbf5b2aa035d6cd9b796d74252045e2b7156ad02/src/OT/Layout/GSUB/SingleSubstFormat1.hh#L55>
412        if num_glyphs >= mask as usize {
413            return Ok(());
414        }
415
416        let intersection = coverage.intersect_set(ctx.parent_active_glyphs());
417        if intersection.is_empty() {
418            return Ok(());
419        }
420
421        // help fuzzer
422        // ref: <https://github.com/harfbuzz/harfbuzz/blob/fbf5b2aa035d6cd9b796d74252045e2b7156ad02/src/OT/Layout/GSUB/SingleSubstFormat1.hh#L61>
423        let d = self.delta_glyph_id() as i32;
424        let mask = mask as i32;
425        let min_before = intersection.first().unwrap().to_u32() as i32;
426        let max_before = intersection.last().unwrap().to_u32() as i32;
427        let min_after = (min_before + d) & mask;
428        let max_after = (max_before + d) & mask;
429
430        if intersection.len() == (max_before - min_before + 1) as u64
431            && ((min_before <= min_after && min_after <= max_before)
432                || (min_before <= max_after && max_after <= max_before))
433        {
434            return Ok(());
435        }
436
437        for g in intersection.iter() {
438            let new_g = (g.to_u32() as i32 + d) & mask;
439            ctx.add(GlyphId::from(new_g as u32));
440        }
441        Ok(())
442    }
443}
444
445impl GlyphClosure for SingleSubstFormat2<'_> {
446    fn closure_glyphs(
447        &self,
448        ctx: &mut ClosureCtx,
449        _lookup_list: &SubstitutionLookupList,
450        _lookup_index: u16,
451    ) -> Result<(), ReadError> {
452        if self.coverage_offset().is_null() || self.glyph_count() == 0 {
453            return Ok(());
454        }
455        let coverage = self.coverage()?;
456        let glyph_set = ctx.parent_active_glyphs();
457        let subs_glyphs = self.substitute_glyph_ids();
458
459        let new_glyphs: Vec<GlyphId> =
460            if self.glyph_count() as u64 > glyph_set.len() * coverage.cost() as u64 {
461                glyph_set
462                    .iter()
463                    .filter_map(|g| coverage.get(g))
464                    .filter_map(|idx| {
465                        subs_glyphs
466                            .get(idx as usize)
467                            .map(|new_g| GlyphId::from(new_g.get()))
468                    })
469                    .collect()
470            } else {
471                coverage
472                    .iter()
473                    .zip(subs_glyphs)
474                    .filter(|&(g, _)| glyph_set.contains(GlyphId::from(g)))
475                    .map(|(_, &new_g)| GlyphId::from(new_g.get()))
476                    .collect()
477            };
478        ctx.add_glyphs(new_glyphs);
479        Ok(())
480    }
481}
482
483impl GlyphClosure for MultipleSubstFormat1<'_> {
484    fn closure_glyphs(
485        &self,
486        ctx: &mut ClosureCtx,
487        _lookup_list: &SubstitutionLookupList,
488        _lookup_index: u16,
489    ) -> Result<(), ReadError> {
490        if self.coverage_offset().is_null() || self.sequence_count() == 0 {
491            return Ok(());
492        }
493        let coverage = self.coverage()?;
494        let glyph_set = ctx.parent_active_glyphs();
495        let sequences = self.sequences();
496
497        let new_glyphs: Vec<GlyphId> =
498            if self.sequence_count() as u64 > glyph_set.len() * coverage.cost() as u64 {
499                glyph_set
500                    .iter()
501                    .filter_map(|g| coverage.get(g))
502                    .filter_map(|idx| sequences.get(idx as usize).ok())
503                    .flat_map(|seq| {
504                        seq.substitute_glyph_ids()
505                            .iter()
506                            .map(|new_g| GlyphId::from(new_g.get()))
507                    })
508                    .collect()
509            } else {
510                coverage
511                    .iter()
512                    .zip(sequences.iter_as_nullable())
513                    .filter_map(|(g, seq)| {
514                        glyph_set
515                            .contains(GlyphId::from(g))
516                            .then(|| seq.transpose().ok().flatten())
517                            .flatten()
518                    })
519                    .flat_map(|seq| {
520                        seq.substitute_glyph_ids()
521                            .iter()
522                            .map(|new_g| GlyphId::from(new_g.get()))
523                    })
524                    .collect()
525            };
526
527        ctx.add_glyphs(new_glyphs);
528        Ok(())
529    }
530}
531
532impl GlyphClosure for AlternateSubstFormat1<'_> {
533    fn closure_glyphs(
534        &self,
535        ctx: &mut ClosureCtx,
536        _lookup_list: &SubstitutionLookupList,
537        _lookup_index: u16,
538    ) -> Result<(), ReadError> {
539        if self.coverage_offset().is_null() || self.alternate_set_count() == 0 {
540            return Ok(());
541        }
542        let coverage = self.coverage()?;
543        let glyph_set = ctx.parent_active_glyphs();
544        let alts = self.alternate_sets();
545
546        let new_glyphs: Vec<GlyphId> =
547            if self.alternate_set_count() as u64 > glyph_set.len() * coverage.cost() as u64 {
548                glyph_set
549                    .iter()
550                    .filter_map(|g| coverage.get(g))
551                    .filter_map(|idx| alts.get(idx as usize).ok())
552                    .flat_map(|alt_set| {
553                        alt_set
554                            .alternate_glyph_ids()
555                            .iter()
556                            .map(|new_g| GlyphId::from(new_g.get()))
557                    })
558                    .collect()
559            } else {
560                coverage
561                    .iter()
562                    .zip(alts.iter_as_nullable())
563                    .filter_map(|(g, alt_set)| {
564                        glyph_set
565                            .contains(GlyphId::from(g))
566                            .then(|| alt_set.transpose().ok().flatten())
567                            .flatten()
568                    })
569                    .flat_map(|alt_set| {
570                        alt_set
571                            .alternate_glyph_ids()
572                            .iter()
573                            .map(|new_g| GlyphId::from(new_g.get()))
574                    })
575                    .collect()
576            };
577
578        ctx.add_glyphs(new_glyphs);
579        Ok(())
580    }
581}
582
583impl GlyphClosure for LigatureSubstFormat1<'_> {
584    fn closure_glyphs(
585        &self,
586        ctx: &mut ClosureCtx,
587        _lookup_list: &SubstitutionLookupList,
588        _lookup_index: u16,
589    ) -> Result<(), ReadError> {
590        if self.coverage_offset().is_null() || self.ligature_set_count() == 0 {
591            return Ok(());
592        }
593        let coverage = self.coverage()?;
594        let ligs = self.ligature_sets();
595        let lig_set_idxes: Vec<usize> =
596            if self.ligature_set_count() as u64 > ctx.parent_active_glyphs().len() {
597                ctx.parent_active_glyphs()
598                    .iter()
599                    .filter_map(|g| coverage.get(g))
600                    .map(|idx| idx as usize)
601                    .collect()
602            } else {
603                coverage
604                    .iter()
605                    .enumerate()
606                    .filter(|&(_idx, g)| ctx.parent_active_glyphs().contains(GlyphId::from(g)))
607                    .map(|(idx, _)| idx)
608                    .collect()
609            };
610
611        for idx in lig_set_idxes {
612            let lig_set = match ligs.get(idx) {
613                Err(ReadError::NullOffset) => continue,
614                other => other,
615            }?;
616            for lig in lig_set.ligatures().iter_as_nullable() {
617                let Some(lig) = lig.transpose()? else {
618                    continue;
619                };
620                if lig.intersects(ctx.glyphs())? {
621                    ctx.add(GlyphId::from(lig.ligature_glyph()));
622                }
623            }
624        }
625        Ok(())
626    }
627}
628
629impl GlyphClosure for ReverseChainSingleSubstFormat1<'_> {
630    fn closure_glyphs(
631        &self,
632        ctx: &mut ClosureCtx,
633        _lookup_list: &SubstitutionLookupList,
634        _lookup_index: u16,
635    ) -> Result<(), ReadError> {
636        if !self.intersects(ctx.glyphs())? {
637            return Ok(());
638        }
639
640        let coverage = self.coverage()?;
641        let glyph_set = ctx.parent_active_glyphs();
642        let idxes: Vec<usize> = if self.glyph_count() as u64 > glyph_set.len() {
643            glyph_set
644                .iter()
645                .filter_map(|g| coverage.get(g))
646                .map(|idx| idx as usize)
647                .collect()
648        } else {
649            coverage
650                .iter()
651                .enumerate()
652                .filter(|&(_idx, g)| glyph_set.contains(GlyphId::from(g)))
653                .map(|(idx, _)| idx)
654                .collect()
655        };
656
657        let sub_glyphs = self.substitute_glyph_ids();
658        for i in idxes {
659            let Some(g) = sub_glyphs.get(i) else {
660                continue;
661            };
662            ctx.add(GlyphId::from(g.get()));
663        }
664
665        Ok(())
666    }
667}
668
669impl GlyphClosure for SequenceContext<'_> {
670    fn closure_glyphs(
671        &self,
672        ctx: &mut ClosureCtx,
673        lookup_list: &SubstitutionLookupList,
674        lookup_index: u16,
675    ) -> Result<(), ReadError> {
676        match self {
677            Self::Format1(table) => {
678                ContextFormat1::Plain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
679            }
680            Self::Format2(table) => {
681                ContextFormat2::Plain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
682            }
683            Self::Format3(table) => {
684                ContextFormat3::Plain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
685            }
686        }
687    }
688}
689
690impl GlyphClosure for ChainedSequenceContext<'_> {
691    fn closure_glyphs(
692        &self,
693        ctx: &mut ClosureCtx,
694        lookup_list: &SubstitutionLookupList,
695        lookup_index: u16,
696    ) -> Result<(), ReadError> {
697        match self {
698            Self::Format1(table) => {
699                ContextFormat1::Chain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
700            }
701            Self::Format2(table) => {
702                ContextFormat2::Chain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
703            }
704            Self::Format3(table) => {
705                ContextFormat3::Chain(table.clone()).closure_glyphs(ctx, lookup_list, lookup_index)
706            }
707        }
708    }
709}
710
711//https://github.com/fonttools/fonttools/blob/a6f59a4f8/Lib/fontTools/subset/__init__.py#L1182
712impl GlyphClosure for ContextFormat1<'_> {
713    fn closure_glyphs(
714        &self,
715        ctx: &mut ClosureCtx,
716        lookup_list: &SubstitutionLookupList,
717        _lookup_index: u16,
718    ) -> Result<(), ReadError> {
719        let Some(coverage) = self.coverage().transpose()? else {
720            return Ok(());
721        };
722        let cov_active_glyphs = coverage.intersect_set(ctx.parent_active_glyphs());
723        if cov_active_glyphs.is_empty() {
724            return Ok(());
725        }
726
727        let lookups = lookup_list.lookups();
728        for (gid, rule_set) in coverage
729            .iter()
730            .zip(self.rule_sets())
731            .filter_map(|(g, rule_set)| {
732                rule_set
733                    .filter(|_| cov_active_glyphs.contains(GlyphId::from(g)))
734                    .map(|rs| (g, rs))
735            })
736        {
737            if ctx.lookup_limit_exceed() {
738                return Ok(());
739            }
740
741            for rule in rule_set?.rules() {
742                if ctx.lookup_limit_exceed() {
743                    return Ok(());
744                }
745                let Some(rule) = rule.transpose()? else {
746                    continue;
747                };
748                if !rule.intersects(ctx.glyphs())? {
749                    continue;
750                }
751
752                let input_seq = rule.input_sequence();
753                let input_count = input_seq.len() + 1;
754                // python calls this 'chaos'. Basically: if there are multiple
755                // lookups applied at a single position they can interact, and
756                // we can no longer trivially determine the state of the context
757                // at that point. In this case we give up, and assume that the
758                // second lookup is reachable by all glyphs.
759                let mut seen_sequence_indices = IntSet::new();
760
761                for lookup_record in rule.lookup_records() {
762                    let lookup_index = lookup_record.lookup_list_index();
763                    let lookup = match lookups.get(lookup_index as usize) {
764                        Err(ReadError::NullOffset) | Err(ReadError::InvalidCollectionIndex(_)) => {
765                            continue
766                        }
767                        other => other,
768                    }?;
769
770                    let sequence_idx = lookup_record.sequence_index();
771                    if sequence_idx as usize >= input_count {
772                        continue;
773                    }
774
775                    let mut active_glyphs = IntSet::empty();
776                    if !seen_sequence_indices.insert(sequence_idx) {
777                        // During processing, when we see an empty set we will replace
778                        // it with the full current glyph set
779                        active_glyphs.extend(ctx.glyphs().iter());
780                    } else if sequence_idx == 0 {
781                        active_glyphs.insert(GlyphId::from(gid));
782                    } else {
783                        let g = input_seq[sequence_idx as usize - 1].get();
784                        active_glyphs.insert(GlyphId::from(g));
785                    };
786
787                    ctx.recurse(
788                        lookup_list,
789                        &lookup,
790                        lookup_index,
791                        active_glyphs,
792                        &mut seen_sequence_indices,
793                        sequence_idx,
794                        input_count as u16,
795                    )?;
796                }
797            }
798        }
799        Ok(())
800    }
801}
802
803//https://github.com/fonttools/fonttools/blob/a6f59a4f87a0111/Lib/fontTools/subset/__init__.py#L1215
804impl GlyphClosure for ContextFormat2<'_> {
805    fn closure_glyphs(
806        &self,
807        ctx: &mut ClosureCtx,
808        lookup_list: &SubstitutionLookupList,
809        _lookup_index: u16,
810    ) -> Result<(), ReadError> {
811        let Some(coverage) = self.coverage().transpose()? else {
812            return Ok(());
813        };
814        let cov_active_glyphs = coverage.intersect_set(ctx.parent_active_glyphs());
815        if cov_active_glyphs.is_empty() {
816            return Ok(());
817        }
818
819        let Some(input_class_def) = self.input_class_def().transpose()? else {
820            return Ok(());
821        };
822        let coverage_glyph_classes = input_class_def.intersect_classes(&cov_active_glyphs);
823        if coverage_glyph_classes.is_empty() {
824            return Ok(());
825        }
826
827        let input_glyph_classes = input_class_def.intersect_classes(ctx.glyphs());
828        let backtrack_classes = match self {
829            Self::Plain(_) => IntSet::empty(),
830            Self::Chain(table) => {
831                if table.backtrack_class_def_offset().is_null() {
832                    IntSet::empty()
833                } else {
834                    table.backtrack_class_def()?.intersect_classes(ctx.glyphs())
835                }
836            }
837        };
838        let lookahead_classes = match self {
839            Self::Plain(_) => IntSet::empty(),
840            Self::Chain(table) => {
841                if table.lookahead_class_def_offset().is_null() {
842                    IntSet::empty()
843                } else {
844                    table.lookahead_class_def()?.intersect_classes(ctx.glyphs())
845                }
846            }
847        };
848
849        let lookups = lookup_list.lookups();
850        for (i, rule_set) in self
851            .rule_sets()
852            .enumerate()
853            .filter_map(|(class, rs)| rs.map(|rs| (class as u16, rs)))
854            .filter(|&(class, _)| coverage_glyph_classes.contains(class))
855        {
856            if ctx.lookup_limit_exceed() {
857                return Ok(());
858            }
859
860            for rule in rule_set?.rules() {
861                if ctx.lookup_limit_exceed() {
862                    return Ok(());
863                }
864                let Some(rule) = rule.transpose()? else {
865                    continue;
866                };
867                if !rule.intersects(&input_glyph_classes, &backtrack_classes, &lookahead_classes) {
868                    continue;
869                }
870
871                let input_seq = rule.input_sequence();
872                let input_count = input_seq.len() + 1;
873
874                let mut seen_sequence_indices = IntSet::new();
875
876                for lookup_record in rule.lookup_records() {
877                    let lookup_index = lookup_record.lookup_list_index();
878                    let lookup = match lookups.get(lookup_index as usize) {
879                        Err(ReadError::NullOffset) | Err(ReadError::InvalidCollectionIndex(_)) => {
880                            continue
881                        }
882                        other => other,
883                    }?;
884                    let sequence_idx = lookup_record.sequence_index();
885                    if sequence_idx as usize >= input_count {
886                        continue;
887                    }
888
889                    let active_glyphs = if !seen_sequence_indices.insert(sequence_idx) {
890                        ctx.glyphs().clone()
891                    } else if sequence_idx == 0 {
892                        input_class_def.intersected_class_glyphs(ctx.parent_active_glyphs(), i)
893                    } else {
894                        let c = input_seq[sequence_idx as usize - 1].get();
895                        input_class_def.intersected_class_glyphs(ctx.glyphs(), c)
896                    };
897
898                    ctx.recurse(
899                        lookup_list,
900                        &lookup,
901                        lookup_index,
902                        active_glyphs,
903                        &mut seen_sequence_indices,
904                        sequence_idx,
905                        input_count as u16,
906                    )?;
907                }
908            }
909        }
910        Ok(())
911    }
912}
913
914impl GlyphClosure for ContextFormat3<'_> {
915    fn closure_glyphs(
916        &self,
917        ctx: &mut ClosureCtx,
918        lookup_list: &SubstitutionLookupList,
919        _lookup_index: u16,
920    ) -> Result<(), ReadError> {
921        if !self.intersects(ctx.glyphs())? {
922            return Ok(());
923        }
924
925        let mut seen_sequence_indices = IntSet::new();
926        let input_coverages = self.coverages();
927        let input_count = input_coverages.len();
928        let lookups = lookup_list.lookups();
929        for record in self.lookup_records() {
930            let lookup_index = record.lookup_list_index();
931            let lookup = match lookups.get(lookup_index as usize) {
932                Err(ReadError::NullOffset) | Err(ReadError::InvalidCollectionIndex(_)) => continue,
933                other => other,
934            }?;
935
936            let seq_idx = record.sequence_index();
937            if seq_idx as usize >= input_count {
938                continue;
939            }
940
941            let active_glyphs = if !seen_sequence_indices.insert(seq_idx) {
942                ctx.glyphs().clone()
943            } else if seq_idx == 0 {
944                let cov = input_coverages.get(0)?;
945                cov.intersect_set(ctx.parent_active_glyphs())
946            } else {
947                let cov = input_coverages.get(seq_idx as usize)?;
948                cov.intersect_set(ctx.glyphs())
949            };
950
951            ctx.recurse(
952                lookup_list,
953                &lookup,
954                lookup_index,
955                active_glyphs,
956                &mut seen_sequence_indices,
957                seq_idx,
958                input_count as u16 + 1,
959            )?;
960        }
961        Ok(())
962    }
963}
964
965impl SubstitutionLookupList<'_> {
966    pub fn closure_lookups(
967        &self,
968        glyph_set: &IntSet<GlyphId>,
969        lookup_indices: &mut IntSet<u16>,
970    ) -> Result<(), ReadError> {
971        lookup_indices.remove_range(self.lookup_count()..=u16::MAX);
972        if lookup_indices.is_empty() {
973            return Ok(());
974        }
975        let lookup_list = LayoutLookupList::Gsub(self);
976        let mut c = LookupClosureCtx::new(glyph_set, &lookup_list);
977
978        let lookups = self.lookups();
979        for idx in lookup_indices.iter() {
980            let lookup = match lookups.get(idx as usize) {
981                Err(ReadError::NullOffset) => {
982                    c.set_lookup_inactive(idx);
983                    continue;
984                }
985                other => other,
986            }?;
987            lookup.closure_lookups(&mut c, idx)?;
988        }
989
990        lookup_indices.union(c.visited_lookups());
991        lookup_indices.subtract(c.inactive_lookups());
992        Ok(())
993    }
994}
995
996impl LookupClosure for SubstitutionLookup<'_> {
997    fn closure_lookups(
998        &self,
999        c: &mut LookupClosureCtx,
1000        lookup_index: u16,
1001    ) -> Result<(), ReadError> {
1002        if !c.should_visit_lookup(lookup_index) {
1003            return Ok(());
1004        }
1005
1006        if !self.intersects(c.glyphs())? {
1007            c.set_lookup_inactive(lookup_index);
1008            return Ok(());
1009        }
1010
1011        self.subtables()?.closure_lookups(c, lookup_index)
1012    }
1013}
1014
1015impl Intersect for SubstitutionLookup<'_> {
1016    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1017        self.subtables()?.intersects(glyph_set)
1018    }
1019}
1020
1021impl LookupClosure for SubstitutionSubtables<'_> {
1022    fn closure_lookups(&self, c: &mut LookupClosureCtx, arg: u16) -> Result<(), ReadError> {
1023        match self {
1024            SubstitutionSubtables::ChainContextual(subtables) => subtables.closure_lookups(c, arg),
1025            SubstitutionSubtables::Contextual(subtables) => subtables.closure_lookups(c, arg),
1026            _ => Ok(()),
1027        }
1028    }
1029}
1030
1031impl Intersect for SubstitutionSubtables<'_> {
1032    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1033        match self {
1034            SubstitutionSubtables::Single(subtables) => subtables.intersects(glyph_set),
1035            SubstitutionSubtables::Multiple(subtables) => subtables.intersects(glyph_set),
1036            SubstitutionSubtables::Alternate(subtables) => subtables.intersects(glyph_set),
1037            SubstitutionSubtables::Ligature(subtables) => subtables.intersects(glyph_set),
1038            SubstitutionSubtables::Contextual(subtables) => subtables.intersects(glyph_set),
1039            SubstitutionSubtables::ChainContextual(subtables) => subtables.intersects(glyph_set),
1040            SubstitutionSubtables::Reverse(subtables) => subtables.intersects(glyph_set),
1041            SubstitutionSubtables::EmptyExtension => Ok(false),
1042        }
1043    }
1044}
1045
1046impl Intersect for SingleSubst<'_> {
1047    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1048        match self {
1049            Self::Format1(item) => item.intersects(glyph_set),
1050            Self::Format2(item) => item.intersects(glyph_set),
1051        }
1052    }
1053}
1054
1055impl Intersect for SingleSubstFormat1<'_> {
1056    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1057        if self.coverage_offset().is_null() {
1058            return Ok(false);
1059        }
1060        Ok(self.coverage()?.intersects(glyph_set))
1061    }
1062}
1063
1064impl Intersect for SingleSubstFormat2<'_> {
1065    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1066        if self.coverage_offset().is_null() {
1067            return Ok(false);
1068        }
1069        Ok(self.coverage()?.intersects(glyph_set))
1070    }
1071}
1072
1073impl Intersect for MultipleSubstFormat1<'_> {
1074    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1075        if self.coverage_offset().is_null() {
1076            return Ok(false);
1077        }
1078        Ok(self.coverage()?.intersects(glyph_set))
1079    }
1080}
1081
1082impl Intersect for AlternateSubstFormat1<'_> {
1083    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1084        if self.coverage_offset().is_null() {
1085            return Ok(false);
1086        }
1087        Ok(self.coverage()?.intersects(glyph_set))
1088    }
1089}
1090
1091impl Intersect for LigatureSubstFormat1<'_> {
1092    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1093        if self.coverage_offset().is_null() {
1094            return Ok(false);
1095        }
1096        let coverage = self.coverage()?;
1097        let lig_sets = self.ligature_sets();
1098        for lig_set in coverage
1099            .iter()
1100            .zip(lig_sets.iter_as_nullable())
1101            .filter_map(|(g, lig_set)| glyph_set.contains(GlyphId::from(g)).then_some(lig_set))
1102        {
1103            let Some(lig_set) = lig_set.transpose()? else {
1104                continue;
1105            };
1106            if lig_set.intersects(glyph_set)? {
1107                return Ok(true);
1108            }
1109        }
1110        Ok(false)
1111    }
1112}
1113
1114impl Intersect for LigatureSet<'_> {
1115    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1116        let ligs = self.ligatures();
1117        for lig in ligs.iter_as_nullable().flatten() {
1118            if lig?.intersects(glyph_set)? {
1119                return Ok(true);
1120            }
1121        }
1122        Ok(false)
1123    }
1124}
1125
1126impl Intersect for Ligature<'_> {
1127    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1128        let ret = self
1129            .component_glyph_ids()
1130            .iter()
1131            .all(|g| glyph_set.contains(GlyphId::from(g.get())));
1132        Ok(ret)
1133    }
1134}
1135
1136impl Intersect for ReverseChainSingleSubstFormat1<'_> {
1137    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
1138        if self.coverage_offset().is_null() {
1139            return Ok(false);
1140        }
1141        if !self.coverage()?.intersects(glyph_set) {
1142            return Ok(false);
1143        }
1144
1145        for coverage in self.backtrack_coverages().iter_as_nullable() {
1146            let Some(coverage) = coverage.transpose()? else {
1147                return Ok(false);
1148            };
1149            if !coverage.intersects(glyph_set) {
1150                return Ok(false);
1151            }
1152        }
1153
1154        for coverage in self.lookahead_coverages().iter_as_nullable() {
1155            let Some(coverage) = coverage.transpose()? else {
1156                return Ok(false);
1157            };
1158            if !coverage.intersects(glyph_set) {
1159                return Ok(false);
1160            }
1161        }
1162        Ok(true)
1163    }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168    use std::collections::{HashMap, HashSet};
1169
1170    use crate::{FontRef, TableProvider};
1171
1172    use super::*;
1173    use font_test_data::closure as test_data;
1174
1175    struct GlyphMap {
1176        to_gid: HashMap<&'static str, GlyphId>,
1177        from_gid: HashMap<GlyphId, &'static str>,
1178    }
1179
1180    impl GlyphMap {
1181        fn new(raw_order: &'static str) -> GlyphMap {
1182            let to_gid: HashMap<_, _> = raw_order
1183                .split('\n')
1184                .map(|line| line.trim())
1185                .filter(|line| !(line.starts_with('#') || line.is_empty()))
1186                .enumerate()
1187                .map(|(gid, name)| (name, GlyphId::new(gid.try_into().unwrap())))
1188                .collect();
1189            let from_gid = to_gid.iter().map(|(name, gid)| (*gid, *name)).collect();
1190            GlyphMap { from_gid, to_gid }
1191        }
1192
1193        fn get_gid(&self, name: &str) -> Option<GlyphId> {
1194            self.to_gid.get(name).copied()
1195        }
1196
1197        fn get_name(&self, gid: GlyphId) -> Option<&str> {
1198            self.from_gid.get(&gid).copied()
1199        }
1200    }
1201
1202    fn get_gsub(test_data: &'static [u8]) -> Gsub<'static> {
1203        let font = FontRef::new(test_data).unwrap();
1204        font.gsub().unwrap()
1205    }
1206
1207    fn compute_closure(gsub: &Gsub, glyph_map: &GlyphMap, input: &[&str]) -> IntSet<GlyphId> {
1208        let lookup_indices = gsub.collect_lookups(&IntSet::all()).unwrap();
1209        let mut input_glyphs = input
1210            .iter()
1211            .map(|name| glyph_map.get_gid(name).unwrap())
1212            .collect();
1213        gsub.closure_glyphs(&lookup_indices, &mut input_glyphs)
1214            .unwrap();
1215        input_glyphs
1216    }
1217
1218    /// assert a set of glyph ids matches a slice of names
1219    macro_rules! assert_closure_result {
1220        ($glyph_map:expr, $result:expr, $expected:expr) => {
1221            let result = $result
1222                .iter()
1223                .map(|gid| $glyph_map.get_name(gid).unwrap())
1224                .collect::<HashSet<_>>();
1225            let expected = $expected.iter().copied().collect::<HashSet<_>>();
1226            if expected != result {
1227                let in_output = result.difference(&expected).collect::<Vec<_>>();
1228                let in_expected = expected.difference(&result).collect::<Vec<_>>();
1229                let mut msg = format!("Closure output does not match\n");
1230                if !in_expected.is_empty() {
1231                    msg.push_str(format!("missing {in_expected:?}\n").as_str());
1232                }
1233                if !in_output.is_empty() {
1234                    msg.push_str(format!("unexpected {in_output:?}").as_str());
1235                }
1236                panic!("{msg}")
1237            }
1238        };
1239    }
1240
1241    #[test]
1242    fn smoke_test() {
1243        // tests various lookup types.
1244        // test input is font-test-data/test_data/fea/simple_closure.fea
1245        let gsub = get_gsub(test_data::SIMPLE);
1246        let glyph_map = GlyphMap::new(test_data::SIMPLE_GLYPHS);
1247        let result = compute_closure(&gsub, &glyph_map, &["a"]);
1248
1249        assert_closure_result!(
1250            glyph_map,
1251            result,
1252            &["a", "A", "b", "c", "d", "a_a", "a.1", "a.2", "a.3"]
1253        );
1254    }
1255
1256    #[test]
1257    fn recursive() {
1258        // a scenario in which one substitution adds glyphs that trigger additional
1259        // substitutions.
1260        //
1261        // test input is font-test-data/test_data/fea/recursive_closure.fea
1262        let gsub = get_gsub(test_data::RECURSIVE);
1263        let glyph_map = GlyphMap::new(test_data::RECURSIVE_GLYPHS);
1264        let result = compute_closure(&gsub, &glyph_map, &["a"]);
1265        assert_closure_result!(glyph_map, result, &["a", "b", "c", "d"]);
1266    }
1267
1268    #[test]
1269    fn contextual_lookups_nop() {
1270        let gsub = get_gsub(test_data::CONTEXTUAL);
1271        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1272
1273        // these match the lookups but not the context
1274        let nop = compute_closure(&gsub, &glyph_map, &["three", "four", "e", "f"]);
1275        assert_closure_result!(glyph_map, nop, &["three", "four", "e", "f"]);
1276    }
1277
1278    #[test]
1279    fn contextual_lookups_chained_f1() {
1280        let gsub = get_gsub(test_data::CONTEXTUAL);
1281        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1282        let gsub6f1 = compute_closure(
1283            &gsub,
1284            &glyph_map,
1285            &["one", "two", "three", "four", "five", "six", "seven"],
1286        );
1287        assert_closure_result!(
1288            glyph_map,
1289            gsub6f1,
1290            &["one", "two", "three", "four", "five", "six", "seven", "X", "Y"]
1291        );
1292    }
1293
1294    #[test]
1295    fn contextual_lookups_chained_f3() {
1296        let gsub = get_gsub(test_data::CONTEXTUAL);
1297        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1298        let gsub6f3 = compute_closure(&gsub, &glyph_map, &["space", "e"]);
1299        assert_closure_result!(glyph_map, gsub6f3, &["space", "e", "e.2"]);
1300
1301        let gsub5f3 = compute_closure(&gsub, &glyph_map, &["f", "g"]);
1302        assert_closure_result!(glyph_map, gsub5f3, &["f", "g", "f.2"]);
1303    }
1304
1305    #[test]
1306    fn contextual_plain_f1() {
1307        let gsub = get_gsub(test_data::CONTEXTUAL);
1308        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1309        let gsub5f1 = compute_closure(&gsub, &glyph_map, &["a", "b"]);
1310        assert_closure_result!(glyph_map, gsub5f1, &["a", "b", "a_b"]);
1311    }
1312
1313    #[test]
1314    fn contextual_plain_f3() {
1315        let gsub = get_gsub(test_data::CONTEXTUAL);
1316        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1317        let gsub5f3 = compute_closure(&gsub, &glyph_map, &["f", "g"]);
1318        assert_closure_result!(glyph_map, gsub5f3, &["f", "g", "f.2"]);
1319    }
1320
1321    #[test]
1322    fn recursive_context() {
1323        let gsub = get_gsub(test_data::RECURSIVE_CONTEXTUAL);
1324        let glyph_map = GlyphMap::new(test_data::RECURSIVE_CONTEXTUAL_GLYPHS);
1325
1326        let nop = compute_closure(&gsub, &glyph_map, &["b", "B"]);
1327        assert_closure_result!(glyph_map, nop, &["b", "B"]);
1328
1329        let full = compute_closure(&gsub, &glyph_map, &["a", "b", "c"]);
1330        assert_closure_result!(glyph_map, full, &["a", "b", "c", "B", "B.2", "B.3"]);
1331
1332        let intermediate = compute_closure(&gsub, &glyph_map, &["a", "B.2"]);
1333        assert_closure_result!(glyph_map, intermediate, &["a", "B.2", "B.3"]);
1334    }
1335
1336    #[test]
1337    fn feature_variations() {
1338        let gsub = get_gsub(test_data::VARIATIONS_CLOSURE);
1339        let glyph_map = GlyphMap::new(test_data::VARIATIONS_GLYPHS);
1340
1341        let input = compute_closure(&gsub, &glyph_map, &["a"]);
1342        assert_closure_result!(glyph_map, input, &["a", "b", "c"]);
1343    }
1344
1345    #[test]
1346    fn chain_context_format3() {
1347        let gsub = get_gsub(test_data::CHAIN_CONTEXT_FORMAT3_BITS);
1348        let glyph_map = GlyphMap::new(test_data::CHAIN_CONTEXT_FORMAT3_BITS_GLYPHS);
1349
1350        let nop = compute_closure(&gsub, &glyph_map, &["c", "z"]);
1351        assert_closure_result!(glyph_map, nop, &["c", "z"]);
1352
1353        let full = compute_closure(&gsub, &glyph_map, &["a", "b", "c", "z"]);
1354        assert_closure_result!(glyph_map, full, &["a", "b", "c", "z", "A", "B"]);
1355    }
1356
1357    #[test]
1358    fn closure_ignore_unreachable_glyphs() {
1359        let font = FontRef::new(font_test_data::closure::CONTEXT_ONLY_REACHABLE).unwrap();
1360        let gsub = font.gsub().unwrap();
1361        let glyph_map = GlyphMap::new(test_data::CONTEXT_ONLY_REACHABLE_GLYPHS);
1362        let result = compute_closure(&gsub, &glyph_map, &["a", "b", "c", "d", "e", "f", "period"]);
1363        assert_closure_result!(
1364            glyph_map,
1365            result,
1366            &["a", "b", "c", "d", "e", "f", "period", "A", "B", "C"]
1367        );
1368    }
1369
1370    #[test]
1371    fn cyclical_context() {
1372        let gsub = get_gsub(test_data::CYCLIC_CONTEXTUAL);
1373        let glyph_map = GlyphMap::new(test_data::RECURSIVE_CONTEXTUAL_GLYPHS);
1374        // we mostly care that this terminates
1375        let nop = compute_closure(&gsub, &glyph_map, &["a", "b", "c"]);
1376        assert_closure_result!(glyph_map, nop, &["a", "b", "c"]);
1377    }
1378
1379    #[test]
1380    fn collect_all_features() {
1381        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1382        let gsub = font.gsub().unwrap();
1383        let ret = gsub
1384            .collect_features(&IntSet::all(), &IntSet::all(), &IntSet::all())
1385            .unwrap();
1386        assert_eq!(ret.len(), 2);
1387        assert!(ret.contains(0));
1388        assert!(ret.contains(1));
1389    }
1390
1391    #[test]
1392    fn collect_all_features_with_feature_filter() {
1393        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1394        let gsub = font.gsub().unwrap();
1395
1396        let mut feature_tags = IntSet::empty();
1397        feature_tags.insert(Tag::new(b"SUB5"));
1398
1399        let ret = gsub
1400            .collect_features(&IntSet::all(), &IntSet::all(), &feature_tags)
1401            .unwrap();
1402        assert_eq!(ret.len(), 1);
1403        assert!(ret.contains(0));
1404    }
1405
1406    #[test]
1407    fn collect_all_features_with_script_filter() {
1408        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1409        let gsub = font.gsub().unwrap();
1410
1411        let mut script_tags = IntSet::empty();
1412        script_tags.insert(Tag::new(b"LATN"));
1413
1414        let ret = gsub
1415            .collect_features(&script_tags, &IntSet::all(), &IntSet::all())
1416            .unwrap();
1417        assert!(ret.is_empty());
1418    }
1419}