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        }
369        false
370    }
371}
372
373#[derive(Clone)]
374enum ClusterShaperKind<'a> {
375    Nominal,
376    SingleFeature(Feature<'a>),
377}
378
379/// Captures glyphs from the GSUB table that aren't present in cmap.
380///
381/// FreeType does this in a few phases:
382/// 1. Collect all lookups for a given set of scripts and features.
383///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L174>
384/// 2. For each lookup, collect all _output_ glyphs.
385///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L201>
386/// 3. If the style represents a specific feature, make sure at least one of
387///    the characters in the associated blue string would be substituted by
388///    those lookups. If none would be substituted, then we don't assign the
389///    style to any glyphs because we don't have any modified alignment
390///    zones.
391///    <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L264>
392///
393/// We roll these into one pass over the lookups below so that we don't have
394/// to allocate a lookup set or iterate them twice. Note that since
395/// substitutions are checked for individual characters, we ignore ligatures
396/// and contextual lookups (and alternates since they aren't applicable).
397struct GsubHandler<'a, 'b> {
398    charmap: &'a Charmap<'a>,
399    lookup_list: &'a SubstitutionLookupList<'a>,
400    style: &'a StyleClass,
401    glyph_styles: &'a mut [GlyphStyle],
402    // Set to true when we need to check if any substitutions are available
403    // for our blue strings. This is the case when style.feature != None
404    need_blue_substs: bool,
405    // Keep track of our range of touched gids in the style list
406    min_gid: usize,
407    max_gid: usize,
408    lookup_depth: usize,
409    visited_set: &'a mut VisitedLookupSet<'b>,
410}
411
412impl<'a, 'b> GsubHandler<'a, 'b> {
413    fn new(
414        charmap: &'a Charmap<'a>,
415        lookup_list: &'a SubstitutionLookupList,
416        style: &'a StyleClass,
417        glyph_styles: &'a mut [GlyphStyle],
418        visited_set: &'a mut VisitedLookupSet<'b>,
419    ) -> Self {
420        let min_gid = glyph_styles.len();
421        // If we have a feature, then we need to check the blue string to see
422        // if any substitutions are available. If not, we don't enable this
423        // style because it won't have any affect on alignment zones
424        let need_blue_substs = style.feature.is_some();
425        Self {
426            charmap,
427            lookup_list,
428            style,
429            glyph_styles,
430            need_blue_substs,
431            min_gid,
432            max_gid: 0,
433            lookup_depth: 0,
434            visited_set,
435        }
436    }
437
438    fn process_lookup(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
439        // General protection against stack overflows
440        if self.lookup_depth == MAX_NESTING_DEPTH {
441            return Err(ProcessLookupError::ExceededMaxDepth);
442        }
443        // Skip lookups that have already been processed
444        if !self.visited_set.insert(lookup_index) {
445            return Ok(());
446        }
447        self.lookup_depth += 1;
448        // Actually process the lookup
449        let result = self.process_lookup_inner(lookup_index);
450        // Out we go again
451        self.lookup_depth -= 1;
452        result
453    }
454
455    #[inline(always)]
456    fn process_lookup_inner(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
457        let Ok(subtables) = self
458            .lookup_list
459            .lookups()
460            .get(lookup_index as usize)
461            .and_then(|lookup| lookup.subtables())
462        else {
463            return Ok(());
464        };
465        match subtables {
466            SubstitutionSubtables::Single(tables) => {
467                for table in tables.iter().filter_map(|table| table.ok()) {
468                    match table {
469                        SingleSubst::Format1(table) => {
470                            let Ok(coverage) = table.coverage() else {
471                                continue;
472                            };
473                            let delta = table.delta_glyph_id() as i32;
474                            for gid in coverage.iter() {
475                                self.capture_glyph((gid.to_u32() as i32 + delta) as u16 as u32);
476                            }
477                            // Check input coverage for blue strings if
478                            // required and if we're not under a contextual
479                            // lookup
480                            if self.need_blue_substs && self.lookup_depth == 1 {
481                                self.check_blue_coverage(Ok(coverage));
482                            }
483                        }
484                        SingleSubst::Format2(table) => {
485                            for gid in table.substitute_glyph_ids() {
486                                self.capture_glyph(gid.get().to_u32());
487                            }
488                            // See above
489                            if self.need_blue_substs && self.lookup_depth == 1 {
490                                self.check_blue_coverage(table.coverage());
491                            }
492                        }
493                    }
494                }
495            }
496            SubstitutionSubtables::Multiple(tables) => {
497                for table in tables.iter().filter_map(|table| table.ok()) {
498                    for seq in table.sequences().iter().filter_map(|seq| seq.ok()) {
499                        for gid in seq.substitute_glyph_ids() {
500                            self.capture_glyph(gid.get().to_u32());
501                        }
502                    }
503                    // See above
504                    if self.need_blue_substs && self.lookup_depth == 1 {
505                        self.check_blue_coverage(table.coverage());
506                    }
507                }
508            }
509            SubstitutionSubtables::Ligature(tables) => {
510                for table in tables.iter().filter_map(|table| table.ok()) {
511                    for set in table.ligature_sets().iter().filter_map(|set| set.ok()) {
512                        for lig in set.ligatures().iter().filter_map(|lig| lig.ok()) {
513                            self.capture_glyph(lig.ligature_glyph().to_u32());
514                        }
515                    }
516                }
517            }
518            SubstitutionSubtables::Alternate(tables) => {
519                for table in tables.iter().filter_map(|table| table.ok()) {
520                    for set in table.alternate_sets().iter().filter_map(|set| set.ok()) {
521                        for gid in set.alternate_glyph_ids() {
522                            self.capture_glyph(gid.get().to_u32());
523                        }
524                    }
525                }
526            }
527            SubstitutionSubtables::Contextual(tables) => {
528                for table in tables.iter().filter_map(|table| table.ok()) {
529                    match table {
530                        SequenceContext::Format1(table) => {
531                            for set in table
532                                .seq_rule_sets()
533                                .iter()
534                                .filter_map(|set| set.transpose().ok().flatten())
535                            {
536                                for rule in set.seq_rules().iter().filter_map(|rule| rule.ok()) {
537                                    for rec in rule.seq_lookup_records() {
538                                        self.process_lookup(rec.lookup_list_index())?;
539                                    }
540                                }
541                            }
542                        }
543                        SequenceContext::Format2(table) => {
544                            for set in table
545                                .class_seq_rule_sets()
546                                .iter()
547                                .filter_map(|set| set.transpose().ok().flatten())
548                            {
549                                for rule in
550                                    set.class_seq_rules().iter().filter_map(|rule| rule.ok())
551                                {
552                                    for rec in rule.seq_lookup_records() {
553                                        self.process_lookup(rec.lookup_list_index())?;
554                                    }
555                                }
556                            }
557                        }
558                        SequenceContext::Format3(table) => {
559                            for rec in table.seq_lookup_records() {
560                                self.process_lookup(rec.lookup_list_index())?;
561                            }
562                        }
563                    }
564                }
565            }
566            SubstitutionSubtables::ChainContextual(tables) => {
567                for table in tables.iter().filter_map(|table| table.ok()) {
568                    match table {
569                        ChainedSequenceContext::Format1(table) => {
570                            for set in table
571                                .chained_seq_rule_sets()
572                                .iter()
573                                .filter_map(|set| set.transpose().ok().flatten())
574                            {
575                                for rule in
576                                    set.chained_seq_rules().iter().filter_map(|rule| rule.ok())
577                                {
578                                    for rec in rule.seq_lookup_records() {
579                                        self.process_lookup(rec.lookup_list_index())?;
580                                    }
581                                }
582                            }
583                        }
584                        ChainedSequenceContext::Format2(table) => {
585                            for set in table
586                                .chained_class_seq_rule_sets()
587                                .iter()
588                                .filter_map(|set| set.transpose().ok().flatten())
589                            {
590                                for rule in set
591                                    .chained_class_seq_rules()
592                                    .iter()
593                                    .filter_map(|rule| rule.ok())
594                                {
595                                    for rec in rule.seq_lookup_records() {
596                                        self.process_lookup(rec.lookup_list_index())?;
597                                    }
598                                }
599                            }
600                        }
601                        ChainedSequenceContext::Format3(table) => {
602                            for rec in table.seq_lookup_records() {
603                                self.process_lookup(rec.lookup_list_index())?;
604                            }
605                        }
606                    }
607                }
608            }
609            SubstitutionSubtables::Reverse(tables) => {
610                for table in tables.iter().filter_map(|table| table.ok()) {
611                    for gid in table.substitute_glyph_ids() {
612                        self.capture_glyph(gid.get().to_u32());
613                    }
614                }
615            }
616        }
617        Ok(())
618    }
619
620    /// Finishes processing for this set of GSUB lookups and
621    /// returns the range of touched glyphs.
622    fn finish(self) -> Option<Range<usize>> {
623        self.visited_set.clear();
624        if self.min_gid > self.max_gid {
625            // We didn't touch any glyphs
626            return None;
627        }
628        let range = self.min_gid..self.max_gid + 1;
629        if self.need_blue_substs {
630            // We didn't find any substitutions for our blue strings so
631            // we ignore the style. Clear the GSUB marker for any touched
632            // glyphs
633            for glyph in &mut self.glyph_styles[range] {
634                glyph.clear_from_gsub();
635            }
636            None
637        } else {
638            Some(range)
639        }
640    }
641
642    /// Checks the given coverage table for any characters in the blue
643    /// strings associated with our current style.
644    fn check_blue_coverage(&mut self, coverage: Result<CoverageTable<'a>, ReadError>) {
645        let Ok(coverage) = coverage else {
646            return;
647        };
648        for (blue_str, _) in self.style.script.blues {
649            if blue_str
650                .chars()
651                .filter_map(|ch| self.charmap.map(ch))
652                .filter_map(|gid| coverage.get(gid))
653                .next()
654                .is_some()
655            {
656                // Condition satisfied, so don't check any further subtables
657                self.need_blue_substs = false;
658                return;
659            }
660        }
661    }
662
663    fn capture_glyph(&mut self, gid: u32) {
664        let gid = gid as usize;
665        if let Some(style) = self.glyph_styles.get_mut(gid) {
666            style.set_from_gsub_output();
667            self.min_gid = gid.min(self.min_gid);
668            self.max_gid = gid.max(self.max_gid);
669        }
670    }
671}
672
673pub(crate) struct VisitedLookupSet<'a>(&'a mut [u8]);
674
675impl<'a> VisitedLookupSet<'a> {
676    pub fn new(storage: &'a mut [u8]) -> Self {
677        Self(storage)
678    }
679
680    /// If the given lookup index is not already in the set, adds it and
681    /// returns `true`. Returns `false` otherwise.
682    ///
683    /// This follows the behavior of `HashSet::insert`.
684    fn insert(&mut self, lookup_index: u16) -> bool {
685        let byte_ix = lookup_index as usize / 8;
686        let bit_mask = 1 << (lookup_index % 8) as u8;
687        if let Some(byte) = self.0.get_mut(byte_ix) {
688            if *byte & bit_mask == 0 {
689                *byte |= bit_mask;
690                true
691            } else {
692                false
693            }
694        } else {
695            false
696        }
697    }
698
699    fn clear(&mut self) {
700        self.0.fill(0);
701    }
702}
703
704#[derive(PartialEq, Debug)]
705enum ProcessLookupError {
706    ExceededMaxDepth,
707}
708
709#[cfg(test)]
710mod tests {
711    use super::{super::style, *};
712    use font_test_data::bebuffer::BeBuffer;
713    use raw::{FontData, FontRead};
714
715    #[test]
716    fn small_caps_subst() {
717        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
718        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
719        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
720        let mut cluster_shaper = shaper.cluster_shaper(style);
721        let mut cluster = ShapedCluster::new();
722        cluster_shaper.shape("H", &mut cluster);
723        assert_eq!(cluster.len(), 1);
724        // from ttx, gid 8 is small caps "H"
725        assert_eq!(cluster[0].id, GlyphId::new(8));
726    }
727
728    #[test]
729    fn small_caps_nominal() {
730        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
731        let shaper = Shaper::new(&font, ShaperMode::Nominal);
732        let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
733        let mut cluster_shaper = shaper.cluster_shaper(style);
734        let mut cluster = ShapedCluster::new();
735        cluster_shaper.shape("H", &mut cluster);
736        assert_eq!(cluster.len(), 1);
737        // from ttx, gid 1 is "H"
738        assert_eq!(cluster[0].id, GlyphId::new(1));
739    }
740
741    #[test]
742    fn exceed_max_depth() {
743        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
744        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
745        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
746        // Build a lookup chain exceeding our max depth
747        let mut bad_lookup_builder = BadLookupBuilder::default();
748        for i in 0..MAX_NESTING_DEPTH {
749            // each lookup calls the next
750            bad_lookup_builder.lookups.push(i as u16 + 1);
751        }
752        let lookup_list_buf = bad_lookup_builder.build();
753        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
754        let mut set_buf = [0u8; 8192];
755        let mut visited_set = VisitedLookupSet(&mut set_buf);
756        let mut gsub_handler = GsubHandler::new(
757            &shaper.charmap,
758            &lookup_list,
759            style,
760            &mut [],
761            &mut visited_set,
762        );
763        assert_eq!(
764            gsub_handler.process_lookup(0),
765            Err(ProcessLookupError::ExceededMaxDepth)
766        );
767    }
768
769    #[test]
770    fn dont_cycle_forever() {
771        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
772        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
773        let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
774        // Build a lookup chain that cycles; 0 calls 1 which calls 0
775        let mut bad_lookup_builder = BadLookupBuilder::default();
776        bad_lookup_builder.lookups.push(1);
777        bad_lookup_builder.lookups.push(0);
778        let lookup_list_buf = bad_lookup_builder.build();
779        let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
780        let mut set_buf = [0u8; 8192];
781        let mut visited_set = VisitedLookupSet(&mut set_buf);
782        let mut gsub_handler = GsubHandler::new(
783            &shaper.charmap,
784            &lookup_list,
785            style,
786            &mut [],
787            &mut visited_set,
788        );
789        gsub_handler.process_lookup(0).unwrap();
790    }
791
792    #[test]
793    fn visited_set() {
794        let count = 2341u16;
795        let n_bytes = (count as usize).div_ceil(8);
796        let mut set_buf = vec![0u8; n_bytes];
797        let mut set = VisitedLookupSet::new(&mut set_buf);
798        for i in 0..count {
799            assert!(set.insert(i));
800            assert!(!set.insert(i));
801        }
802        for byte in &set_buf[0..set_buf.len() - 1] {
803            assert_eq!(*byte, 0xFF);
804        }
805        assert_eq!(*set_buf.last().unwrap(), 0b00011111);
806    }
807
808    #[derive(Default)]
809    struct BadLookupBuilder {
810        /// Just a list of nested lookup indices for each generated lookup
811        lookups: Vec<u16>,
812    }
813
814    impl BadLookupBuilder {
815        fn build(&self) -> Vec<u8> {
816            // Full byte size of a contextual format 3 lookup with one
817            // subtable and one nested lookup
818            const CONTEXT3_FULL_SIZE: usize = 18;
819            let mut buf = BeBuffer::default();
820            // LookupList table
821            // count
822            buf = buf.push(self.lookups.len() as u16);
823            // offsets for each lookup
824            let base_offset = 2 + 2 * self.lookups.len();
825            for i in 0..self.lookups.len() {
826                buf = buf.push((base_offset + i * CONTEXT3_FULL_SIZE) as u16);
827            }
828            // now the actual lookups
829            for nested_ix in &self.lookups {
830                // lookup type: GSUB contextual substitution
831                buf = buf.push(5u16);
832                // lookup flag
833                buf = buf.push(0u16);
834                // subtable count
835                buf = buf.push(1u16);
836                // offset to single subtable (always 8 bytes from start of lookup)
837                buf = buf.push(8u16);
838                // start of subtable, format == 3
839                buf = buf.push(3u16);
840                // number of glyphs in sequence
841                buf = buf.push(0u16);
842                // sequence lookup count
843                buf = buf.push(1u16);
844                // (no coverage offsets)
845                // sequence lookup (sequence index, lookup index)
846                buf = buf.push(0u16).push(*nested_ix);
847            }
848            buf.to_vec()
849        }
850    }
851}