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