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