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