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