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