Skip to main content

skrifa/outline/autohint/
shape.rs

1//! Shaping support for autohinting.
2
3use super::style::{GlyphStyle, StyleClass};
4use crate::{charmap::Charmap, collections::SmallVec, FontRef, GlyphId, MetadataProvider};
5use core::ops::Range;
6use raw::{
7    tables::{
8        gsub::{
9            ChainedSequenceContext, Gsub, SequenceContext, SingleSubst, SubstitutionLookupList,
10            SubstitutionSubtables,
11        },
12        layout::{Feature, ScriptTags},
13        varc::CoverageTable,
14    },
15    types::Tag,
16    ReadError, TableProvider,
17};
18
19// To prevent infinite recursion in contextual lookups. Matches HB
20// <https://github.com/harfbuzz/harfbuzz/blob/c7ef6a2ed58ae8ec108ee0962bef46f42c73a60c/src/hb-limits.hh#L53>
21const MAX_NESTING_DEPTH: usize = 64;
22
23/// Determines the fidelity with which we apply shaping in the
24/// autohinter.
25///
26/// Shaping only affects glyph style classification and the glyphs that
27/// are chosen for metrics computations. We keep the `Nominal` mode around
28/// to enable validation of internal algorithms against a configuration that
29/// is known to match FreeType. The `BestEffort` mode should always be
30/// used for actual rendering.
31#[derive(Copy, Clone, PartialEq, Eq, Debug)]
32pub(crate) enum ShaperMode {
33    /// Characters are mapped to nominal glyph identifiers and layout tables
34    /// are not used for style coverage.
35    ///
36    /// This matches FreeType when HarfBuzz support is not enabled.
37    Nominal,
38    /// Simple substitutions are applied according to script rules and layout
39    /// tables are used to extend style coverage beyond the character map.
40    #[allow(unused)]
41    BestEffort,
42}
43
44#[derive(Copy, Clone, Default, Debug)]
45pub(crate) struct ShapedGlyph {
46    pub id: GlyphId,
47    /// This may be used for computing vertical alignment zones, particularly
48    /// for glyphs like super/subscripts which might have adjustments in GPOS.
49    ///
50    /// Note that we don't do the same in the horizontal direction which
51    /// means that we don't care about the x-offset.
52    pub y_offset: i32,
53}
54
55/// Arbitrarily chosen to cover our max input size plus some extra to account
56/// for expansion from multiple substitution tables.
57const SHAPED_CLUSTER_INLINE_SIZE: usize = 16;
58
59/// Container for storing the result of shaping a cluster.
60///
61/// Some of our input "characters" for metrics computations are actually
62/// multi-character [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)
63/// that may expand to multiple glyphs.
64pub(crate) type ShapedCluster = SmallVec<ShapedGlyph, SHAPED_CLUSTER_INLINE_SIZE>;
65
66#[derive(Copy, Clone, PartialEq, Eq, Debug)]
67pub(crate) enum ShaperCoverageKind {
68    /// Shaper coverage that traverses a specific script.
69    Script,
70    /// Shaper coverage that also includes the `Dflt` script.
71    ///
72    /// This is used as a catch all after all styles are processed.
73    Default,
74}
75
76/// Maps characters to glyphs and handles extended style coverage beyond
77/// glyphs that are available in the character map.
78///
79/// Roughly covers the functionality in <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c>.
80pub(crate) struct Shaper<'a> {
81    font: FontRef<'a>,
82    #[allow(unused)]
83    mode: ShaperMode,
84    charmap: Charmap<'a>,
85    gsub: Option<Gsub<'a>>,
86}
87
88impl<'a> Shaper<'a> {
89    pub fn new(font: &FontRef<'a>, mode: ShaperMode) -> Self {
90        let charmap = font.charmap();
91        let gsub = (mode != ShaperMode::Nominal)
92            .then(|| font.gsub().ok())
93            .flatten();
94        Self {
95            font: font.clone(),
96            mode,
97            charmap,
98            gsub,
99        }
100    }
101
102    pub fn font(&self) -> &FontRef<'a> {
103        &self.font
104    }
105
106    pub fn charmap(&self) -> &Charmap<'a> {
107        &self.charmap
108    }
109
110    pub fn lookup_count(&self) -> u16 {
111        self.gsub
112            .as_ref()
113            .and_then(|gsub| gsub.lookup_list().ok())
114            .map(|list| list.lookup_count())
115            .unwrap_or_default()
116    }
117
118    pub fn cluster_shaper(&'a self, style: &StyleClass) -> ClusterShaper<'a> {
119        if self.mode == ShaperMode::BestEffort {
120            // For now, only apply substitutions for styles with an associated
121            // feature
122            if let Some(feature_tag) = style.feature {
123                if let Some((lookup_list, feature)) = self.gsub.as_ref().and_then(|gsub| {
124                    let script_list = gsub.script_list().ok()?;
125                    let selected_script =
126                        script_list.select(&ScriptTags::from_unicode(style.script.tag))?;
127                    let script = script_list.get(selected_script.index).ok()?;
128                    let lang_sys = script.default_lang_sys()?.ok()?;
129                    let feature_list = gsub.feature_list().ok()?;
130                    let feature_ix = lang_sys.feature_index_for_tag(&feature_list, feature_tag)?;
131                    let feature = feature_list.get(feature_ix).ok()?.element;
132                    let lookup_list = gsub.lookup_list().ok()?;
133                    Some((lookup_list, feature))
134                }) {
135                    return ClusterShaper {
136                        shaper: self,
137                        lookup_list: Some(lookup_list),
138                        kind: ClusterShaperKind::SingleFeature(feature),
139                    };
140                }
141            }
142        }
143        ClusterShaper {
144            shaper: self,
145            lookup_list: None,
146            kind: ClusterShaperKind::Nominal,
147        }
148    }
149
150    /// Uses layout tables to compute coverage for the given style.
151    ///
152    /// Returns `true` if any glyph styles were updated for this style.
153    ///
154    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L99>
155    pub(crate) fn compute_coverage(
156        &self,
157        style: &StyleClass,
158        coverage_kind: ShaperCoverageKind,
159        glyph_styles: &mut [GlyphStyle],
160        visited_set: &mut VisitedLookupSet<'_>,
161    ) -> bool {
162        let Some(gsub) = self.gsub.as_ref() else {
163            return false;
164        };
165        let (Ok(script_list), Ok(feature_list), Ok(lookup_list)) =
166            (gsub.script_list(), gsub.feature_list(), gsub.lookup_list())
167        else {
168            return false;
169        };
170        let mut script_tags: [Option<Tag>; 3] = [None; 3];
171        for (a, b) in script_tags
172            .iter_mut()
173            .zip(ScriptTags::from_unicode(style.script.tag).iter())
174        {
175            *a = Some(*b);
176        }
177        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L153>
178        const DEFAULT_SCRIPT: Tag = Tag::new(b"DFLT");
179        if coverage_kind == ShaperCoverageKind::Default {
180            if script_tags[0].is_none() {
181                script_tags[0] = Some(DEFAULT_SCRIPT);
182            } else if script_tags[1].is_none() {
183                script_tags[1] = Some(DEFAULT_SCRIPT);
184            } else if script_tags[1] != Some(DEFAULT_SCRIPT) {
185                script_tags[2] = Some(DEFAULT_SCRIPT);
186            }
187        } else {
188            // Script classes contain some non-standard tags used for special
189            // purposes. We ignore these
190            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L167>
191            const NON_STANDARD_TAGS: &[Option<Tag>] = &[
192                // Khmer symbols
193                Some(Tag::new(b"Khms")),
194                // Latin subscript fallbacks
195                Some(Tag::new(b"Latb")),
196                // Latin superscript fallbacks
197                Some(Tag::new(b"Latp")),
198            ];
199            if NON_STANDARD_TAGS.contains(&script_tags[0]) {
200                return false;
201            }
202        }
203        // Check each requested script that is available in GSUB
204        let mut gsub_handler = GsubHandler::new(
205            &self.charmap,
206            &lookup_list,
207            style,
208            glyph_styles,
209            visited_set,
210        );
211        for script in script_tags.iter().filter_map(|tag| {
212            tag.and_then(|tag| script_list.index_for_tag(tag))
213                .and_then(|ix| script_list.script_records().get(ix as usize))
214                .and_then(|rec| rec.script(script_list.offset_data()).ok())
215        }) {
216            // And all language systems for each script
217            for langsys in script
218                .lang_sys_records()
219                .iter()
220                .filter_map(|rec| rec.lang_sys(script.offset_data()).ok())
221                .chain(script.default_lang_sys().transpose().ok().flatten())
222            {
223                for feature_ix in langsys.feature_indices() {
224                    let Some(feature) = feature_list
225                        .feature_records()
226                        .get(feature_ix.get() as usize)
227                        .and_then(|rec| {
228                            // If our style has a feature tag, we only look at that specific
229                            // feature; otherwise, handle all of them
230                            if style.feature == Some(rec.feature_tag()) || style.feature.is_none() {
231                                rec.feature(feature_list.offset_data()).ok()
232                            } else {
233                                None
234                            }
235                        })
236                    else {
237                        continue;
238                    };
239                    // And now process associated lookups
240                    for index in feature.lookup_list_indices().iter() {
241                        // We only care about errors here for testing
242                        let _ = gsub_handler.process_lookup(index.get());
243                    }
244                }
245            }
246        }
247        if let Some(range) = gsub_handler.finish() {
248            // If we get a range then we captured at least some glyphs so
249            // let's try to assign our current style
250            let mut result = false;
251            for glyph_style in &mut glyph_styles[range] {
252                // We only want to return true here if we actually assign the
253                // style to avoid computing unnecessary metrics
254                result |= glyph_style.maybe_assign_gsub_output_style(style);
255            }
256            result
257        } else {
258            false
259        }
260    }
261}
262
263pub(crate) struct ClusterShaper<'a> {
264    shaper: &'a Shaper<'a>,
265    lookup_list: Option<SubstitutionLookupList<'a>>,
266    kind: ClusterShaperKind<'a>,
267}
268
269impl ClusterShaper<'_> {
270    pub(crate) fn shape(&mut self, input: &str, output: &mut ShapedCluster) {
271        // First fill the output cluster with the nominal character
272        // to glyph id mapping
273        output.clear();
274        for ch in input.chars() {
275            output.push(ShapedGlyph {
276                id: self.shaper.charmap.map(ch).unwrap_or_default(),
277                y_offset: 0,
278            });
279        }
280        match self.kind.clone() {
281            ClusterShaperKind::Nominal => {
282                // In nominal mode, reject clusters with multiple glyphs
283                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L639>
284                if self.shaper.mode == ShaperMode::Nominal && output.len() > 1 {
285                    output.clear();
286                }
287            }
288            ClusterShaperKind::SingleFeature(feature) => {
289                let mut did_subst = false;
290                for lookup_ix in feature.lookup_list_indices() {
291                    let mut glyph_ix = 0;
292                    while glyph_ix < output.len() {
293                        did_subst |= self.apply_lookup(lookup_ix.get(), output, glyph_ix, 0);
294                        glyph_ix += 1;
295                    }
296                }
297                // Reject clusters that weren't modified by the feature.
298                // FreeType detects this by shaping twice and comparing gids
299                // but we just track substitutions
300                // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L528>
301                if !did_subst {
302                    output.clear();
303                }
304            }
305        }
306    }
307
308    fn apply_lookup(
309        &self,
310        lookup_index: u16,
311        cluster: &mut ShapedCluster,
312        glyph_ix: usize,
313        nesting_depth: usize,
314    ) -> bool {
315        if nesting_depth > MAX_NESTING_DEPTH {
316            return false;
317        }
318        let Some(glyph) = cluster.get_mut(glyph_ix) else {
319            return false;
320        };
321        let Some(subtables) = self
322            .lookup_list
323            .as_ref()
324            .and_then(|list| list.lookups().get(lookup_index as usize).ok())
325            .and_then(|lookup| lookup.subtables().ok())
326        else {
327            return false;
328        };
329        match subtables {
330            // For now, just applying single substitutions because we're
331            // currently only handling shaping for "feature" styles like
332            // c2sc (caps to small caps) which are (almost?) always
333            // single substs
334            SubstitutionSubtables::Single(tables) => {
335                for table in tables.iter().filter_map(|table| table.ok()) {
336                    match table {
337                        SingleSubst::Format1(table) => {
338                            let Some(_) = table.coverage().ok().and_then(|cov| cov.get(glyph.id))
339                            else {
340                                continue;
341                            };
342                            let delta = table.delta_glyph_id() as i32;
343                            glyph.id = GlyphId::from((glyph.id.to_u32() as i32 + delta) as u16);
344                            return true;
345                        }
346                        SingleSubst::Format2(table) => {
347                            let Some(cov_ix) =
348                                table.coverage().ok().and_then(|cov| cov.get(glyph.id))
349                            else {
350                                continue;
351                            };
352                            let Some(subst) = table.substitute_glyph_ids().get(cov_ix as usize)
353                            else {
354                                continue;
355                            };
356                            glyph.id = subst.get().into();
357                            return true;
358                        }
359                    }
360                }
361            }
362            SubstitutionSubtables::Multiple(_tables) => {}
363            SubstitutionSubtables::Ligature(_tables) => {}
364            SubstitutionSubtables::Alternate(_tables) => {}
365            SubstitutionSubtables::Contextual(_tables) => {}
366            SubstitutionSubtables::ChainContextual(_tables) => {}
367            SubstitutionSubtables::Reverse(_tables) => {}
368            SubstitutionSubtables::EmptyExtension => {}
369        }
370        false
371    }
372}
373
374#[derive(Clone)]
375enum ClusterShaperKind<'a> {
376    Nominal,
377    SingleFeature(Feature<'a>),
378}
379
380/// Captures glyphs from the GSUB table that aren't present in cmap.
381///
382/// FreeType does this in a few phases:
383/// 1. Collect all lookups for a given set of scripts and features.
384///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L174>
385/// 2. For each lookup, collect all _output_ glyphs.
386///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L201>
387/// 3. If the style represents a specific feature, make sure at least one of
388///    the characters in the associated blue string would be substituted by
389///    those lookups. If none would be substituted, then we don't assign the
390///    style to any glyphs because we don't have any modified alignment
391///    zones.
392///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L264>
393///
394/// We roll these into one pass over the lookups below so that we don't have
395/// to allocate a lookup set or iterate them twice. Note that since
396/// substitutions are checked for individual characters, we ignore ligatures
397/// and contextual lookups (and alternates since they aren't applicable).
398struct GsubHandler<'a, 'b> {
399    charmap: &'a Charmap<'a>,
400    lookup_list: &'a SubstitutionLookupList<'a>,
401    style: &'a StyleClass,
402    glyph_styles: &'a mut [GlyphStyle],
403    // Set to true when we need to check if any substitutions are available
404    // for our blue strings. This is the case when style.feature != None
405    need_blue_substs: bool,
406    // Keep track of our range of touched gids in the style list
407    min_gid: usize,
408    max_gid: usize,
409    lookup_depth: usize,
410    visited_set: &'a mut VisitedLookupSet<'b>,
411}
412
413impl<'a, 'b> GsubHandler<'a, 'b> {
414    fn new(
415        charmap: &'a Charmap<'a>,
416        lookup_list: &'a SubstitutionLookupList,
417        style: &'a StyleClass,
418        glyph_styles: &'a mut [GlyphStyle],
419        visited_set: &'a mut VisitedLookupSet<'b>,
420    ) -> Self {
421        let min_gid = glyph_styles.len();
422        // If we have a feature, then we need to check the blue string to see
423        // if any substitutions are available. If not, we don't enable this
424        // style because it won't have any affect on alignment zones
425        let need_blue_substs = style.feature.is_some();
426        Self {
427            charmap,
428            lookup_list,
429            style,
430            glyph_styles,
431            need_blue_substs,
432            min_gid,
433            max_gid: 0,
434            lookup_depth: 0,
435            visited_set,
436        }
437    }
438
439    fn process_lookup(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
440        // General protection against stack overflows
441        if self.lookup_depth == MAX_NESTING_DEPTH {
442            return Err(ProcessLookupError::ExceededMaxDepth);
443        }
444        // Skip lookups that have already been processed
445        if !self.visited_set.insert(lookup_index) {
446            return Ok(());
447        }
448        self.lookup_depth += 1;
449        // Actually process the lookup
450        let result = self.process_lookup_inner(lookup_index);
451        // Out we go again
452        self.lookup_depth -= 1;
453        result
454    }
455
456    #[inline(always)]
457    fn process_lookup_inner(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
458        let Ok(subtables) = self
459            .lookup_list
460            .lookups()
461            .get(lookup_index as usize)
462            .and_then(|lookup| lookup.subtables())
463        else {
464            return Ok(());
465        };
466        match subtables {
467            SubstitutionSubtables::Single(tables) => {
468                for table in tables.iter().filter_map(|table| table.ok()) {
469                    match table {
470                        SingleSubst::Format1(table) => {
471                            let Ok(coverage) = table.coverage() else {
472                                continue;
473                            };
474                            let delta = table.delta_glyph_id() as i32;
475                            for gid in coverage.iter() {
476                                self.capture_glyph((gid.to_u32() as i32 + delta) as u16 as u32);
477                            }
478                            // Check input coverage for blue strings if
479                            // required and if we're not under a contextual
480                            // lookup
481                            if self.need_blue_substs && self.lookup_depth == 1 {
482                                self.check_blue_coverage(Ok(coverage));
483                            }
484                        }
485                        SingleSubst::Format2(table) => {
486                            for gid in table.substitute_glyph_ids() {
487                                self.capture_glyph(gid.get().to_u32());
488                            }
489                            // See above
490                            if self.need_blue_substs && self.lookup_depth == 1 {
491                                self.check_blue_coverage(table.coverage());
492                            }
493                        }
494                    }
495                }
496            }
497            SubstitutionSubtables::Multiple(tables) => {
498                for table in tables.iter().filter_map(|table| table.ok()) {
499                    for seq in table.sequences().iter().filter_map(|seq| seq.ok()) {
500                        for gid in seq.substitute_glyph_ids() {
501                            self.capture_glyph(gid.get().to_u32());
502                        }
503                    }
504                    // See above
505                    if self.need_blue_substs && self.lookup_depth == 1 {
506                        self.check_blue_coverage(table.coverage());
507                    }
508                }
509            }
510            SubstitutionSubtables::Ligature(tables) => {
511                for table in tables.iter().filter_map(|table| table.ok()) {
512                    for set in table.ligature_sets().iter().filter_map(|set| set.ok()) {
513                        for lig in set.ligatures().iter().filter_map(|lig| lig.ok()) {
514                            self.capture_glyph(lig.ligature_glyph().to_u32());
515                        }
516                    }
517                }
518            }
519            SubstitutionSubtables::Alternate(tables) => {
520                for table in tables.iter().filter_map(|table| table.ok()) {
521                    for set in table.alternate_sets().iter().filter_map(|set| set.ok()) {
522                        for gid in set.alternate_glyph_ids() {
523                            self.capture_glyph(gid.get().to_u32());
524                        }
525                    }
526                }
527            }
528            SubstitutionSubtables::Contextual(tables) => {
529                for table in tables.iter().filter_map(|table| table.ok()) {
530                    match table {
531                        SequenceContext::Format1(table) => {
532                            for set in table
533                                .seq_rule_sets()
534                                .iter()
535                                .filter_map(|set| set.transpose().ok().flatten())
536                            {
537                                for rule in set.seq_rules().iter().filter_map(|rule| rule.ok()) {
538                                    for rec in rule.seq_lookup_records() {
539                                        self.process_lookup(rec.lookup_list_index())?;
540                                    }
541                                }
542                            }
543                        }
544                        SequenceContext::Format2(table) => {
545                            for set in table
546                                .class_seq_rule_sets()
547                                .iter()
548                                .filter_map(|set| set.transpose().ok().flatten())
549                            {
550                                for rule in
551                                    set.class_seq_rules().iter().filter_map(|rule| rule.ok())
552                                {
553                                    for rec in rule.seq_lookup_records() {
554                                        self.process_lookup(rec.lookup_list_index())?;
555                                    }
556                                }
557                            }
558                        }
559                        SequenceContext::Format3(table) => {
560                            for rec in table.seq_lookup_records() {
561                                self.process_lookup(rec.lookup_list_index())?;
562                            }
563                        }
564                    }
565                }
566            }
567            SubstitutionSubtables::ChainContextual(tables) => {
568                for table in tables.iter().filter_map(|table| table.ok()) {
569                    match table {
570                        ChainedSequenceContext::Format1(table) => {
571                            for set in table
572                                .chained_seq_rule_sets()
573                                .iter()
574                                .filter_map(|set| set.transpose().ok().flatten())
575                            {
576                                for rule in
577                                    set.chained_seq_rules().iter().filter_map(|rule| rule.ok())
578                                {
579                                    for rec in rule.seq_lookup_records() {
580                                        self.process_lookup(rec.lookup_list_index())?;
581                                    }
582                                }
583                            }
584                        }
585                        ChainedSequenceContext::Format2(table) => {
586                            for set in table
587                                .chained_class_seq_rule_sets()
588                                .iter()
589                                .filter_map(|set| set.transpose().ok().flatten())
590                            {
591                                for rule in set
592                                    .chained_class_seq_rules()
593                                    .iter()
594                                    .filter_map(|rule| rule.ok())
595                                {
596                                    for rec in rule.seq_lookup_records() {
597                                        self.process_lookup(rec.lookup_list_index())?;
598                                    }
599                                }
600                            }
601                        }
602                        ChainedSequenceContext::Format3(table) => {
603                            for rec in table.seq_lookup_records() {
604                                self.process_lookup(rec.lookup_list_index())?;
605                            }
606                        }
607                    }
608                }
609            }
610            SubstitutionSubtables::Reverse(tables) => {
611                for table in tables.iter().filter_map(|table| table.ok()) {
612                    for gid in table.substitute_glyph_ids() {
613                        self.capture_glyph(gid.get().to_u32());
614                    }
615                }
616            }
617            SubstitutionSubtables::EmptyExtension => (),
618        }
619        Ok(())
620    }
621
622    /// Finishes processing for this set of GSUB lookups and
623    /// returns the range of touched glyphs.
624    fn finish(self) -> Option<Range<usize>> {
625        self.visited_set.clear();
626        if self.min_gid > self.max_gid {
627            // We didn't touch any glyphs
628            return None;
629        }
630        let range = self.min_gid..self.max_gid + 1;
631        if self.need_blue_substs {
632            // We didn't find any substitutions for our blue strings so
633            // we ignore the style. Clear the GSUB marker for any touched
634            // glyphs
635            for glyph in &mut self.glyph_styles[range] {
636                glyph.clear_from_gsub();
637            }
638            None
639        } else {
640            Some(range)
641        }
642    }
643
644    /// Checks the given coverage table for any characters in the blue
645    /// strings associated with our current style.
646    fn check_blue_coverage(&mut self, coverage: Result<CoverageTable<'a>, ReadError>) {
647        let Ok(coverage) = coverage else {
648            return;
649        };
650        for (blue_str, _) in self.style.script.blues {
651            if blue_str
652                .chars()
653                .filter_map(|ch| self.charmap.map(ch))
654                .filter_map(|gid| coverage.get(gid))
655                .next()
656                .is_some()
657            {
658                // Condition satisfied, so don't check any further subtables
659                self.need_blue_substs = false;
660                return;
661            }
662        }
663    }
664
665    fn capture_glyph(&mut self, gid: u32) {
666        let gid = gid as usize;
667        if let Some(style) = self.glyph_styles.get_mut(gid) {
668            style.set_from_gsub_output();
669            self.min_gid = gid.min(self.min_gid);
670            self.max_gid = gid.max(self.max_gid);
671        }
672    }
673}
674
675pub(crate) struct VisitedLookupSet<'a>(&'a mut [u8]);
676
677impl<'a> VisitedLookupSet<'a> {
678    pub fn new(storage: &'a mut [u8]) -> Self {
679        Self(storage)
680    }
681
682    /// If the given lookup index is not already in the set, adds it and
683    /// returns `true`. Returns `false` otherwise.
684    ///
685    /// This follows the behavior of `HashSet::insert`.
686    fn insert(&mut self, lookup_index: u16) -> bool {
687        let byte_ix = lookup_index as usize / 8;
688        let bit_mask = 1 << (lookup_index % 8) as u8;
689        if let Some(byte) = self.0.get_mut(byte_ix) {
690            if *byte & bit_mask == 0 {
691                *byte |= bit_mask;
692                true
693            } else {
694                false
695            }
696        } else {
697            false
698        }
699    }
700
701    fn clear(&mut self) {
702        self.0.fill(0);
703    }
704}
705
706#[derive(PartialEq, Debug)]
707enum ProcessLookupError {
708    ExceededMaxDepth,
709}
710
711#[cfg(test)]
712mod tests {
713    use super::{super::style, *};
714    use font_test_data::bebuffer::BeBuffer;
715    use raw::{FontData, FontRead};
716
717    #[test]
718    fn small_caps_subst() {
719        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
720        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
721        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
722        let mut cluster_shaper = shaper.cluster_shaper(style);
723        let mut cluster = ShapedCluster::new();
724        cluster_shaper.shape("H", &mut cluster);
725        assert_eq!(cluster.len(), 1);
726        // from ttx, gid 8 is small caps "H"
727        assert_eq!(cluster[0].id, GlyphId::new(8));
728    }
729
730    #[test]
731    fn small_caps_nominal() {
732        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
733        let shaper = Shaper::new(&font, ShaperMode::Nominal);
734        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
735        let mut cluster_shaper = shaper.cluster_shaper(style);
736        let mut cluster = ShapedCluster::new();
737        cluster_shaper.shape("H", &mut cluster);
738        assert_eq!(cluster.len(), 1);
739        // from ttx, gid 1 is "H"
740        assert_eq!(cluster[0].id, GlyphId::new(1));
741    }
742
743    #[test]
744    fn exceed_max_depth() {
745        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
746        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
747        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
748        // Build a lookup chain exceeding our max depth
749        let mut bad_lookup_builder = BadLookupBuilder::default();
750        for i in 0..MAX_NESTING_DEPTH {
751            // each lookup calls the next
752            bad_lookup_builder.lookups.push(i as u16 + 1);
753        }
754        let lookup_list_buf = bad_lookup_builder.build();
755        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
756        let mut set_buf = [0u8; 8192];
757        let mut visited_set = VisitedLookupSet(&mut set_buf);
758        let mut gsub_handler = GsubHandler::new(
759            &shaper.charmap,
760            &lookup_list,
761            style,
762            &mut [],
763            &mut visited_set,
764        );
765        assert_eq!(
766            gsub_handler.process_lookup(0),
767            Err(ProcessLookupError::ExceededMaxDepth)
768        );
769    }
770
771    #[test]
772    fn dont_cycle_forever() {
773        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
774        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
775        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
776        // Build a lookup chain that cycles; 0 calls 1 which calls 0
777        let mut bad_lookup_builder = BadLookupBuilder::default();
778        bad_lookup_builder.lookups.push(1);
779        bad_lookup_builder.lookups.push(0);
780        let lookup_list_buf = bad_lookup_builder.build();
781        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
782        let mut set_buf = [0u8; 8192];
783        let mut visited_set = VisitedLookupSet(&mut set_buf);
784        let mut gsub_handler = GsubHandler::new(
785            &shaper.charmap,
786            &lookup_list,
787            style,
788            &mut [],
789            &mut visited_set,
790        );
791        gsub_handler.process_lookup(0).unwrap();
792    }
793
794    #[test]
795    fn visited_set() {
796        let count = 2341u16;
797        let n_bytes = (count as usize).div_ceil(8);
798        let mut set_buf = vec![0u8; n_bytes];
799        let mut set = VisitedLookupSet::new(&mut set_buf);
800        for i in 0..count {
801            assert!(set.insert(i));
802            assert!(!set.insert(i));
803        }
804        for byte in &set_buf[0..set_buf.len() - 1] {
805            assert_eq!(*byte, 0xFF);
806        }
807        assert_eq!(*set_buf.last().unwrap(), 0b00011111);
808    }
809
810    #[derive(Default)]
811    struct BadLookupBuilder {
812        /// Just a list of nested lookup indices for each generated lookup
813        lookups: Vec<u16>,
814    }
815
816    impl BadLookupBuilder {
817        fn build(&self) -> Vec<u8> {
818            // Full byte size of a contextual format 3 lookup with one
819            // subtable and one nested lookup
820            const CONTEXT3_FULL_SIZE: usize = 18;
821            let mut buf = BeBuffer::default();
822            // LookupList table
823            // count
824            buf = buf.push(self.lookups.len() as u16);
825            // offsets for each lookup
826            let base_offset = 2 + 2 * self.lookups.len();
827            for i in 0..self.lookups.len() {
828                buf = buf.push((base_offset + i * CONTEXT3_FULL_SIZE) as u16);
829            }
830            // now the actual lookups
831            for nested_ix in &self.lookups {
832                // lookup type: GSUB contextual substitution
833                buf = buf.push(5u16);
834                // lookup flag
835                buf = buf.push(0u16);
836                // subtable count
837                buf = buf.push(1u16);
838                // offset to single subtable (always 8 bytes from start of lookup)
839                buf = buf.push(8u16);
840                // start of subtable, format == 3
841                buf = buf.push(3u16);
842                // number of glyphs in sequence
843                buf = buf.push(0u16);
844                // sequence lookup count
845                buf = buf.push(1u16);
846                // (no coverage offsets)
847                // sequence lookup (sequence index, lookup index)
848                buf = buf.push(0u16).push(*nested_ix);
849            }
850            buf.to_vec()
851        }
852    }
853}