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                ctx.parent_active_glyphs().clone()
833            } else {
834                let cov = input_coverages.get(seq_idx as usize)?;
835                cov.intersect_set(ctx.glyphs())
836            };
837
838            let lookup_index = record.lookup_list_index();
839            let lookup = lookup_list.lookups().get(lookup_index as usize)?;
840            if lookup.may_have_non_1to1()? {
841                seen_sequence_indices.insert_range(seq_idx..=input_count as u16);
842            }
843            ctx.recurse(lookup_list, &lookup, lookup_index, active_glyphs)?;
844        }
845        Ok(())
846    }
847}
848
849impl SubstitutionLookupList<'_> {
850    pub fn closure_lookups(
851        &self,
852        glyph_set: &IntSet<GlyphId>,
853        lookup_indices: &mut IntSet<u16>,
854    ) -> Result<(), ReadError> {
855        let lookup_list = LayoutLookupList::Gsub(self);
856        let mut c = LookupClosureCtx::new(glyph_set, &lookup_list);
857
858        let lookups = self.lookups();
859        for idx in lookup_indices.iter() {
860            let lookup = lookups.get(idx as usize)?;
861            lookup.closure_lookups(&mut c, idx)?;
862        }
863
864        lookup_indices.union(c.visited_lookups());
865        lookup_indices.subtract(c.inactive_lookups());
866        Ok(())
867    }
868}
869
870impl LookupClosure for SubstitutionLookup<'_> {
871    fn closure_lookups(
872        &self,
873        c: &mut LookupClosureCtx,
874        lookup_index: u16,
875    ) -> Result<(), ReadError> {
876        if !c.should_visit_lookup(lookup_index) {
877            return Ok(());
878        }
879
880        if !self.intersects(c.glyphs())? {
881            c.set_lookup_inactive(lookup_index);
882            return Ok(());
883        }
884
885        self.subtables()?.closure_lookups(c, lookup_index)
886    }
887}
888
889impl Intersect for SubstitutionLookup<'_> {
890    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
891        self.subtables()?.intersects(glyph_set)
892    }
893}
894
895impl LookupClosure for SubstitutionSubtables<'_> {
896    fn closure_lookups(&self, c: &mut LookupClosureCtx, arg: u16) -> Result<(), ReadError> {
897        match self {
898            SubstitutionSubtables::ChainContextual(subtables) => subtables.closure_lookups(c, arg),
899            SubstitutionSubtables::Contextual(subtables) => subtables.closure_lookups(c, arg),
900            _ => Ok(()),
901        }
902    }
903}
904
905impl Intersect for SubstitutionSubtables<'_> {
906    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
907        match self {
908            SubstitutionSubtables::Single(subtables) => subtables.intersects(glyph_set),
909            SubstitutionSubtables::Multiple(subtables) => subtables.intersects(glyph_set),
910            SubstitutionSubtables::Alternate(subtables) => subtables.intersects(glyph_set),
911            SubstitutionSubtables::Ligature(subtables) => subtables.intersects(glyph_set),
912            SubstitutionSubtables::Contextual(subtables) => subtables.intersects(glyph_set),
913            SubstitutionSubtables::ChainContextual(subtables) => subtables.intersects(glyph_set),
914            SubstitutionSubtables::Reverse(subtables) => subtables.intersects(glyph_set),
915        }
916    }
917}
918
919impl Intersect for SingleSubst<'_> {
920    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
921        match self {
922            Self::Format1(item) => item.intersects(glyph_set),
923            Self::Format2(item) => item.intersects(glyph_set),
924        }
925    }
926}
927
928impl Intersect for SingleSubstFormat1<'_> {
929    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
930        Ok(self.coverage()?.intersects(glyph_set))
931    }
932}
933
934impl Intersect for SingleSubstFormat2<'_> {
935    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
936        Ok(self.coverage()?.intersects(glyph_set))
937    }
938}
939
940impl Intersect for MultipleSubstFormat1<'_> {
941    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
942        Ok(self.coverage()?.intersects(glyph_set))
943    }
944}
945
946impl Intersect for AlternateSubstFormat1<'_> {
947    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
948        Ok(self.coverage()?.intersects(glyph_set))
949    }
950}
951
952impl Intersect for LigatureSubstFormat1<'_> {
953    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
954        let coverage = self.coverage()?;
955        let lig_sets = self.ligature_sets();
956        for lig_set in coverage
957            .iter()
958            .zip(lig_sets.iter())
959            .filter_map(|(g, lig_set)| glyph_set.contains(GlyphId::from(g)).then_some(lig_set))
960        {
961            if lig_set?.intersects(glyph_set)? {
962                return Ok(true);
963            }
964        }
965        Ok(false)
966    }
967}
968
969impl Intersect for LigatureSet<'_> {
970    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
971        let ligs = self.ligatures();
972        for lig in ligs.iter() {
973            if lig?.intersects(glyph_set)? {
974                return Ok(true);
975            }
976        }
977        Ok(false)
978    }
979}
980
981impl Intersect for Ligature<'_> {
982    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
983        let ret = self
984            .component_glyph_ids()
985            .iter()
986            .all(|g| glyph_set.contains(GlyphId::from(g.get())));
987        Ok(ret)
988    }
989}
990
991impl Intersect for ReverseChainSingleSubstFormat1<'_> {
992    fn intersects(&self, glyph_set: &IntSet<GlyphId>) -> Result<bool, ReadError> {
993        if !self.coverage()?.intersects(glyph_set) {
994            return Ok(false);
995        }
996
997        for coverage in self.backtrack_coverages().iter() {
998            if !coverage?.intersects(glyph_set) {
999                return Ok(false);
1000            }
1001        }
1002
1003        for coverage in self.lookahead_coverages().iter() {
1004            if !coverage?.intersects(glyph_set) {
1005                return Ok(false);
1006            }
1007        }
1008        Ok(true)
1009    }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use std::collections::{HashMap, HashSet};
1015
1016    use crate::{FontRef, TableProvider};
1017
1018    use super::*;
1019    use font_test_data::closure as test_data;
1020
1021    struct GlyphMap {
1022        to_gid: HashMap<&'static str, GlyphId>,
1023        from_gid: HashMap<GlyphId, &'static str>,
1024    }
1025
1026    impl GlyphMap {
1027        fn new(raw_order: &'static str) -> GlyphMap {
1028            let to_gid: HashMap<_, _> = raw_order
1029                .split('\n')
1030                .map(|line| line.trim())
1031                .filter(|line| !(line.starts_with('#') || line.is_empty()))
1032                .enumerate()
1033                .map(|(gid, name)| (name, GlyphId::new(gid.try_into().unwrap())))
1034                .collect();
1035            let from_gid = to_gid.iter().map(|(name, gid)| (*gid, *name)).collect();
1036            GlyphMap { from_gid, to_gid }
1037        }
1038
1039        fn get_gid(&self, name: &str) -> Option<GlyphId> {
1040            self.to_gid.get(name).copied()
1041        }
1042
1043        fn get_name(&self, gid: GlyphId) -> Option<&str> {
1044            self.from_gid.get(&gid).copied()
1045        }
1046    }
1047
1048    fn get_gsub(test_data: &'static [u8]) -> Gsub<'static> {
1049        let font = FontRef::new(test_data).unwrap();
1050        font.gsub().unwrap()
1051    }
1052
1053    fn compute_closure(gsub: &Gsub, glyph_map: &GlyphMap, input: &[&str]) -> IntSet<GlyphId> {
1054        let lookup_indices = gsub.collect_lookups(&IntSet::all()).unwrap();
1055        let mut input_glyphs = input
1056            .iter()
1057            .map(|name| glyph_map.get_gid(name).unwrap())
1058            .collect();
1059        gsub.closure_glyphs(&lookup_indices, &mut input_glyphs)
1060            .unwrap();
1061        input_glyphs
1062    }
1063
1064    /// assert a set of glyph ids matches a slice of names
1065    macro_rules! assert_closure_result {
1066        ($glyph_map:expr, $result:expr, $expected:expr) => {
1067            let result = $result
1068                .iter()
1069                .map(|gid| $glyph_map.get_name(gid).unwrap())
1070                .collect::<HashSet<_>>();
1071            let expected = $expected.iter().copied().collect::<HashSet<_>>();
1072            if expected != result {
1073                let in_output = result.difference(&expected).collect::<Vec<_>>();
1074                let in_expected = expected.difference(&result).collect::<Vec<_>>();
1075                let mut msg = format!("Closure output does not match\n");
1076                if !in_expected.is_empty() {
1077                    msg.push_str(format!("missing {in_expected:?}\n").as_str());
1078                }
1079                if !in_output.is_empty() {
1080                    msg.push_str(format!("unexpected {in_output:?}").as_str());
1081                }
1082                panic!("{msg}")
1083            }
1084        };
1085    }
1086
1087    #[test]
1088    fn smoke_test() {
1089        // tests various lookup types.
1090        // test input is font-test-data/test_data/fea/simple_closure.fea
1091        let gsub = get_gsub(test_data::SIMPLE);
1092        let glyph_map = GlyphMap::new(test_data::SIMPLE_GLYPHS);
1093        let result = compute_closure(&gsub, &glyph_map, &["a"]);
1094
1095        assert_closure_result!(
1096            glyph_map,
1097            result,
1098            &["a", "A", "b", "c", "d", "a_a", "a.1", "a.2", "a.3"]
1099        );
1100    }
1101
1102    #[test]
1103    fn recursive() {
1104        // a scenario in which one substitution adds glyphs that trigger additional
1105        // substitutions.
1106        //
1107        // test input is font-test-data/test_data/fea/recursive_closure.fea
1108        let gsub = get_gsub(test_data::RECURSIVE);
1109        let glyph_map = GlyphMap::new(test_data::RECURSIVE_GLYPHS);
1110        let result = compute_closure(&gsub, &glyph_map, &["a"]);
1111        assert_closure_result!(glyph_map, result, &["a", "b", "c", "d"]);
1112    }
1113
1114    #[test]
1115    fn contextual_lookups_nop() {
1116        let gsub = get_gsub(test_data::CONTEXTUAL);
1117        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1118
1119        // these match the lookups but not the context
1120        let nop = compute_closure(&gsub, &glyph_map, &["three", "four", "e", "f"]);
1121        assert_closure_result!(glyph_map, nop, &["three", "four", "e", "f"]);
1122    }
1123
1124    #[test]
1125    fn contextual_lookups_chained_f1() {
1126        let gsub = get_gsub(test_data::CONTEXTUAL);
1127        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1128        let gsub6f1 = compute_closure(
1129            &gsub,
1130            &glyph_map,
1131            &["one", "two", "three", "four", "five", "six", "seven"],
1132        );
1133        assert_closure_result!(
1134            glyph_map,
1135            gsub6f1,
1136            &["one", "two", "three", "four", "five", "six", "seven", "X", "Y"]
1137        );
1138    }
1139
1140    #[test]
1141    fn contextual_lookups_chained_f3() {
1142        let gsub = get_gsub(test_data::CONTEXTUAL);
1143        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1144        let gsub6f3 = compute_closure(&gsub, &glyph_map, &["space", "e"]);
1145        assert_closure_result!(glyph_map, gsub6f3, &["space", "e", "e.2"]);
1146
1147        let gsub5f3 = compute_closure(&gsub, &glyph_map, &["f", "g"]);
1148        assert_closure_result!(glyph_map, gsub5f3, &["f", "g", "f.2"]);
1149    }
1150
1151    #[test]
1152    fn contextual_plain_f1() {
1153        let gsub = get_gsub(test_data::CONTEXTUAL);
1154        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1155        let gsub5f1 = compute_closure(&gsub, &glyph_map, &["a", "b"]);
1156        assert_closure_result!(glyph_map, gsub5f1, &["a", "b", "a_b"]);
1157    }
1158
1159    #[test]
1160    fn contextual_plain_f3() {
1161        let gsub = get_gsub(test_data::CONTEXTUAL);
1162        let glyph_map = GlyphMap::new(test_data::CONTEXTUAL_GLYPHS);
1163        let gsub5f3 = compute_closure(&gsub, &glyph_map, &["f", "g"]);
1164        assert_closure_result!(glyph_map, gsub5f3, &["f", "g", "f.2"]);
1165    }
1166
1167    #[test]
1168    fn recursive_context() {
1169        let gsub = get_gsub(test_data::RECURSIVE_CONTEXTUAL);
1170        let glyph_map = GlyphMap::new(test_data::RECURSIVE_CONTEXTUAL_GLYPHS);
1171
1172        let nop = compute_closure(&gsub, &glyph_map, &["b", "B"]);
1173        assert_closure_result!(glyph_map, nop, &["b", "B"]);
1174
1175        let full = compute_closure(&gsub, &glyph_map, &["a", "b", "c"]);
1176        assert_closure_result!(glyph_map, full, &["a", "b", "c", "B", "B.2", "B.3"]);
1177
1178        let intermediate = compute_closure(&gsub, &glyph_map, &["a", "B.2"]);
1179        assert_closure_result!(glyph_map, intermediate, &["a", "B.2", "B.3"]);
1180    }
1181
1182    #[test]
1183    fn feature_variations() {
1184        let gsub = get_gsub(test_data::VARIATIONS_CLOSURE);
1185        let glyph_map = GlyphMap::new(test_data::VARIATIONS_GLYPHS);
1186
1187        let input = compute_closure(&gsub, &glyph_map, &["a"]);
1188        assert_closure_result!(glyph_map, input, &["a", "b", "c"]);
1189    }
1190
1191    #[test]
1192    fn chain_context_format3() {
1193        let gsub = get_gsub(test_data::CHAIN_CONTEXT_FORMAT3_BITS);
1194        let glyph_map = GlyphMap::new(test_data::CHAIN_CONTEXT_FORMAT3_BITS_GLYPHS);
1195
1196        let nop = compute_closure(&gsub, &glyph_map, &["c", "z"]);
1197        assert_closure_result!(glyph_map, nop, &["c", "z"]);
1198
1199        let full = compute_closure(&gsub, &glyph_map, &["a", "b", "c", "z"]);
1200        assert_closure_result!(glyph_map, full, &["a", "b", "c", "z", "A", "B", "C"]);
1201    }
1202
1203    #[test]
1204    fn cyclical_context() {
1205        let gsub = get_gsub(test_data::CYCLIC_CONTEXTUAL);
1206        let glyph_map = GlyphMap::new(test_data::RECURSIVE_CONTEXTUAL_GLYPHS);
1207        // we mostly care that this terminates
1208        let nop = compute_closure(&gsub, &glyph_map, &["a", "b", "c"]);
1209        assert_closure_result!(glyph_map, nop, &["a", "b", "c"]);
1210    }
1211
1212    #[test]
1213    fn collect_all_features() {
1214        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1215        let gsub = font.gsub().unwrap();
1216        let ret = gsub
1217            .collect_features(&IntSet::all(), &IntSet::all(), &IntSet::all())
1218            .unwrap();
1219        assert_eq!(ret.len(), 2);
1220        assert!(ret.contains(0));
1221        assert!(ret.contains(1));
1222    }
1223
1224    #[test]
1225    fn collect_all_features_with_feature_filter() {
1226        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1227        let gsub = font.gsub().unwrap();
1228
1229        let mut feature_tags = IntSet::empty();
1230        feature_tags.insert(Tag::new(b"SUB5"));
1231
1232        let ret = gsub
1233            .collect_features(&IntSet::all(), &IntSet::all(), &feature_tags)
1234            .unwrap();
1235        assert_eq!(ret.len(), 1);
1236        assert!(ret.contains(0));
1237    }
1238
1239    #[test]
1240    fn collect_all_features_with_script_filter() {
1241        let font = FontRef::new(font_test_data::closure::CONTEXTUAL).unwrap();
1242        let gsub = font.gsub().unwrap();
1243
1244        let mut script_tags = IntSet::empty();
1245        script_tags.insert(Tag::new(b"LATN"));
1246
1247        let ret = gsub
1248            .collect_features(&script_tags, &IntSet::all(), &IntSet::all())
1249            .unwrap();
1250        assert!(ret.is_empty());
1251    }
1252}