skrifa/outline/autohint/
style.rs

1//! Styles, scripts and glyph style mapping.
2
3use super::metrics::BlueZones;
4use super::shape::{ShaperCoverageKind, VisitedLookupSet};
5use alloc::vec::Vec;
6use raw::types::{GlyphId, Tag};
7
8/// Defines the script and style associated with a single glyph.
9#[derive(Copy, Clone, PartialEq, Eq, Debug)]
10#[repr(transparent)]
11pub(crate) struct GlyphStyle(pub(super) u16);
12
13impl GlyphStyle {
14    // The following flags roughly correspond to those defined in FreeType
15    // here: https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L76
16    // but with different values because we intend to store "meta style"
17    // information differently.
18    const STYLE_INDEX_MASK: u16 = 0xFF;
19    const UNASSIGNED: u16 = Self::STYLE_INDEX_MASK;
20    // A non-base character, perhaps more commonly referred to as a "mark"
21    const NON_BASE: u16 = 0x100;
22    const DIGIT: u16 = 0x200;
23    // Used as intermediate state to mark when a glyph appears as GSUB output
24    // for a given script
25    const FROM_GSUB_OUTPUT: u16 = 0x8000;
26
27    pub const fn is_unassigned(self) -> bool {
28        self.0 & Self::STYLE_INDEX_MASK == Self::UNASSIGNED
29    }
30
31    pub const fn is_non_base(self) -> bool {
32        self.0 & Self::NON_BASE != 0
33    }
34
35    pub const fn is_digit(self) -> bool {
36        self.0 & Self::DIGIT != 0
37    }
38
39    pub fn style_class(self) -> Option<&'static StyleClass> {
40        StyleClass::from_index(self.style_index()?)
41    }
42
43    pub fn style_index(self) -> Option<u16> {
44        let ix = self.0 & Self::STYLE_INDEX_MASK;
45        if ix != Self::UNASSIGNED {
46            Some(ix)
47        } else {
48            None
49        }
50    }
51
52    fn maybe_assign(&mut self, other: Self) {
53        // FreeType walks the style array in order so earlier styles
54        // have precedence. Since we walk the cmap and binary search
55        // on the full range mapping, our styles are mapped in a
56        // different order. This check allows us to replace a currently
57        // mapped style if the new style index is lower which matches
58        // FreeType's behavior.
59        //
60        // Note that we keep the extra bits because FreeType allows
61        // setting the NON_BASE bit to an already mapped style.
62        if other.0 & Self::STYLE_INDEX_MASK <= self.0 & Self::STYLE_INDEX_MASK {
63            self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | other.0;
64        }
65    }
66
67    pub(super) fn set_from_gsub_output(&mut self) {
68        self.0 |= Self::FROM_GSUB_OUTPUT
69    }
70
71    pub(super) fn clear_from_gsub(&mut self) {
72        self.0 &= !Self::FROM_GSUB_OUTPUT;
73    }
74
75    /// Assign a style if we've been marked as GSUB output _and_ the
76    /// we don't currently have an assigned style.
77    ///
78    /// This also clears the GSUB output bit.
79    ///
80    /// Returns `true` if this style was applied.
81    pub(super) fn maybe_assign_gsub_output_style(&mut self, style: &StyleClass) -> bool {
82        let style_ix = style.index as u16;
83        if self.0 & Self::FROM_GSUB_OUTPUT != 0 && self.is_unassigned() {
84            self.clear_from_gsub();
85            self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | style_ix;
86            true
87        } else {
88            false
89        }
90    }
91}
92
93impl Default for GlyphStyle {
94    fn default() -> Self {
95        Self(Self::UNASSIGNED)
96    }
97}
98
99/// Sentinel for unused styles in [`GlyphStyleMap::metrics_map`].
100const UNMAPPED_STYLE: u8 = 0xFF;
101
102/// Maps glyph identifiers to glyph styles.
103///
104/// Also keeps track of the styles that are actually used so we can allocate
105/// an appropriately sized metrics array.
106#[derive(Debug)]
107pub(crate) struct GlyphStyleMap {
108    /// List of styles, indexed by glyph id.
109    styles: Vec<GlyphStyle>,
110    /// Maps an actual style class index into a compacted index for the
111    /// metrics table.
112    ///
113    /// Uses `0xFF` to signify unused styles.
114    metrics_map: [u8; MAX_STYLES],
115    /// Number of metrics styles in use.
116    metrics_count: u8,
117}
118
119impl GlyphStyleMap {
120    /// Computes a new glyph style map for the given glyph count and character
121    /// map.
122    ///
123    /// Roughly based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L126>
124    pub fn new(glyph_count: u32, shaper: &Shaper) -> Self {
125        let lookup_count = shaper.lookup_count() as usize;
126        if lookup_count > 0 {
127            // If we're processing lookups, allocate some temporary memory to
128            // store the visited set
129            let lookup_set_byte_size = lookup_count.div_ceil(8);
130            super::super::memory::with_temporary_memory(lookup_set_byte_size, |bytes| {
131                Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(bytes))
132            })
133        } else {
134            Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(&mut []))
135        }
136    }
137
138    fn new_inner(glyph_count: u32, shaper: &Shaper, mut visited_set: VisitedLookupSet) -> Self {
139        let mut map = Self {
140            styles: vec![GlyphStyle::default(); glyph_count as usize],
141            metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
142            metrics_count: 0,
143        };
144        // Step 1: compute styles for glyphs covered by OpenType features
145        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L233>
146        for style in super::style::STYLE_CLASSES {
147            if style.feature.is_some()
148                && shaper.compute_coverage(
149                    style,
150                    ShaperCoverageKind::Script,
151                    &mut map.styles,
152                    &mut visited_set,
153                )
154            {
155                map.use_style(style.index);
156            }
157        }
158        // Step 2: compute styles for glyphs contained in the cmap
159        // cmap entries are sorted so we keep track of the most recent range to
160        // avoid a binary search per character
161        let mut last_range: Option<(usize, StyleRange)> = None;
162        for (ch, gid) in shaper.charmap().mappings() {
163            let Some(style) = map.styles.get_mut(gid.to_u32() as usize) else {
164                continue;
165            };
166            // Charmaps enumerate in order so we're likely to encounter at least
167            // a few codepoints in the same range.
168            if let Some(last) = last_range {
169                if last.1.contains(ch) {
170                    style.maybe_assign(last.1.style);
171                    continue;
172                }
173            }
174            let ix = match STYLE_RANGES.binary_search_by(|x| x.first.cmp(&ch)) {
175                Ok(i) => i,
176                Err(i) => i.saturating_sub(1),
177            };
178            let Some(range) = STYLE_RANGES.get(ix).copied() else {
179                continue;
180            };
181            if range.contains(ch) {
182                style.maybe_assign(range.style);
183                if let Some(style_ix) = range.style.style_index() {
184                    map.use_style(style_ix as usize);
185                }
186                last_range = Some((ix, range));
187            }
188        }
189        // Step 3a: compute script based coverage
190        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L239>
191        for style in super::style::STYLE_CLASSES {
192            if style.feature.is_none()
193                && shaper.compute_coverage(
194                    style,
195                    ShaperCoverageKind::Script,
196                    &mut map.styles,
197                    &mut visited_set,
198                )
199            {
200                map.use_style(style.index);
201            }
202        }
203        // Step 3b: compute coverage for "default" script which is always set
204        // to Latin in FreeType
205        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L248>
206        let default_style = &STYLE_CLASSES[StyleClass::LATN];
207        if shaper.compute_coverage(
208            default_style,
209            ShaperCoverageKind::Default,
210            &mut map.styles,
211            &mut visited_set,
212        ) {
213            map.use_style(default_style.index);
214        }
215        // Step 4: Assign a default to all remaining glyphs
216        // For some reason, FreeType uses Hani as a default fallback style so
217        // let's do the same.
218        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L69>
219        let mut need_hani = false;
220        for style in map.styles.iter_mut() {
221            if style.is_unassigned() {
222                style.0 &= !GlyphStyle::STYLE_INDEX_MASK;
223                style.0 |= StyleClass::HANI as u16;
224                need_hani = true;
225            }
226        }
227        if need_hani {
228            map.use_style(StyleClass::HANI);
229        }
230        // Step 5: Mark ASCII digits
231        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L251>
232        for digit_char in '0'..='9' {
233            if let Some(style) = shaper
234                .charmap()
235                .map(digit_char)
236                .and_then(|gid| map.styles.get_mut(gid.to_u32() as usize))
237            {
238                style.0 |= GlyphStyle::DIGIT;
239            }
240        }
241        map
242    }
243
244    pub fn style(&self, glyph_id: GlyphId) -> Option<GlyphStyle> {
245        self.styles.get(glyph_id.to_u32() as usize).copied()
246    }
247
248    /// Returns a compacted metrics index for the given glyph style.
249    pub fn metrics_index(&self, style: GlyphStyle) -> Option<usize> {
250        let ix = style.style_index()? as usize;
251        let metrics_ix = *self.metrics_map.get(ix)? as usize;
252        if metrics_ix != UNMAPPED_STYLE as usize {
253            Some(metrics_ix)
254        } else {
255            None
256        }
257    }
258
259    /// Returns the required size of the compacted metrics array.
260    pub fn metrics_count(&self) -> usize {
261        self.metrics_count as usize
262    }
263
264    /// Returns an ordered iterator yielding each style class referenced by
265    /// this map.
266    pub fn metrics_styles(&self) -> impl Iterator<Item = &'static StyleClass> + '_ {
267        // Need to build a reverse map so that these are properly ordered
268        let mut reverse_map = [UNMAPPED_STYLE; MAX_STYLES];
269        for (ix, &entry) in self.metrics_map.iter().enumerate() {
270            if entry != UNMAPPED_STYLE {
271                reverse_map[entry as usize] = ix as u8;
272            }
273        }
274        reverse_map
275            .into_iter()
276            .enumerate()
277            .filter_map(move |(mapped, style_ix)| {
278                if mapped == UNMAPPED_STYLE as usize {
279                    None
280                } else {
281                    STYLE_CLASSES.get(style_ix as usize)
282                }
283            })
284    }
285
286    fn use_style(&mut self, style_ix: usize) {
287        let mapped = &mut self.metrics_map[style_ix];
288        if *mapped == UNMAPPED_STYLE {
289            // This the first time we've seen this style so record
290            // it in the metrics map
291            *mapped = self.metrics_count;
292            self.metrics_count += 1;
293        }
294    }
295}
296
297impl Default for GlyphStyleMap {
298    fn default() -> Self {
299        Self {
300            styles: Default::default(),
301            metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
302            metrics_count: 0,
303        }
304    }
305}
306
307/// Determines which algorithms the autohinter will use while generating
308/// metrics and processing a glyph outline.
309#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
310pub(crate) enum ScriptGroup {
311    /// All scripts that are not CJK or Indic.
312    ///
313    /// FreeType calls this Latin.
314    #[default]
315    Default,
316    Cjk,
317    Indic,
318}
319
320/// Defines the basic properties for each script supported by the
321/// autohinter.
322#[derive(Clone, Debug)]
323pub(crate) struct ScriptClass {
324    #[allow(unused)]
325    pub name: &'static str,
326    /// Group that defines how glyphs belonging to this script are hinted.
327    pub group: ScriptGroup,
328    /// Unicode tag for the script.
329    #[allow(unused)]
330    pub tag: Tag,
331    /// True if outline edges are processed top to bottom.
332    pub hint_top_to_bottom: bool,
333    /// Characters used to define standard width and height of stems.
334    pub std_chars: &'static str,
335    /// "Blue" characters used to define alignment zones.
336    pub blues: &'static [(&'static str, BlueZones)],
337}
338
339/// Defines the basic properties for each style supported by the
340/// autohinter.
341///
342/// There's mostly a 1:1 correspondence between styles and scripts except
343/// in the cases where style coverage is determined by OpenType feature
344/// coverage.
345#[derive(Clone, Debug)]
346pub(crate) struct StyleClass {
347    #[allow(unused)]
348    pub name: &'static str,
349    /// Index of self in the STYLE_CLASSES array.
350    pub index: usize,
351    /// Associated Unicode script.
352    pub script: &'static ScriptClass,
353    /// OpenType feature tag for styles that derive coverage from layout
354    /// tables.
355    #[allow(unused)]
356    pub feature: Option<Tag>,
357}
358
359impl StyleClass {
360    pub(crate) fn from_index(index: u16) -> Option<&'static StyleClass> {
361        STYLE_CLASSES.get(index as usize)
362    }
363}
364
365/// Associates a basic glyph style with a range of codepoints.
366#[derive(Copy, Clone, Debug)]
367pub(super) struct StyleRange {
368    pub first: u32,
369    pub last: u32,
370    pub style: GlyphStyle,
371}
372
373impl StyleRange {
374    pub fn contains(&self, ch: u32) -> bool {
375        (self.first..=self.last).contains(&ch)
376    }
377}
378
379// The following are helpers for generated code.
380const fn base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
381    StyleRange {
382        first,
383        last,
384        style: GlyphStyle(style_index),
385    }
386}
387
388const fn non_base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
389    StyleRange {
390        first,
391        last,
392        style: GlyphStyle(style_index | GlyphStyle::NON_BASE),
393    }
394}
395
396const MAX_STYLES: usize = STYLE_CLASSES.len();
397
398use super::shape::Shaper;
399
400include!("../../../generated/generated_autohint_styles.rs");
401
402#[cfg(test)]
403mod tests {
404    use super::{super::shape::ShaperMode, *};
405    use crate::{raw::TableProvider, FontRef, MetadataProvider};
406
407    /// Ensure that style mapping accurately applies the DIGIT bit to
408    /// ASCII digit glyphs.
409    #[test]
410    fn capture_digit_styles() {
411        let font = FontRef::new(font_test_data::AHEM).unwrap();
412        let shaper = Shaper::new(&font, ShaperMode::Nominal);
413        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
414        let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
415        let charmap = font.charmap();
416        let mut digit_count = 0;
417        for (ch, gid) in charmap.mappings() {
418            let style = style_map.style(gid).unwrap();
419            let is_char_digit = char::from_u32(ch).unwrap().is_ascii_digit();
420            assert_eq!(style.is_digit(), is_char_digit);
421            digit_count += is_char_digit as u32;
422        }
423        // This font has all 10 ASCII digits
424        assert_eq!(digit_count, 10);
425    }
426
427    #[test]
428    fn glyph_styles() {
429        // generated by printf debugging in FreeType
430        // (gid, Option<(script_name, is_non_base_char)>)
431        // where "is_non_base_char" more common means "is_mark"
432        let expected = &[
433            (0, Some(("CJKV ideographs", false))),
434            (1, Some(("Latin", true))),
435            (2, Some(("Armenian", true))),
436            (3, Some(("Hebrew", true))),
437            (4, Some(("Arabic", false))),
438            (5, Some(("Arabic", false))),
439            (6, Some(("Arabic", true))),
440            (7, Some(("Devanagari", true))),
441            (8, Some(("Devanagari", false))),
442            (9, Some(("Bengali", true))),
443            (10, Some(("Bengali", false))),
444            (11, Some(("Gurmukhi", true))),
445            (12, Some(("Gurmukhi", false))),
446            (13, Some(("Gujarati", true))),
447            (14, Some(("Gujarati", true))),
448            (15, Some(("Oriya", true))),
449            (16, Some(("Oriya", false))),
450            (17, Some(("Tamil", true))),
451            (18, Some(("Tamil", false))),
452            (19, Some(("Telugu", true))),
453            (20, Some(("Telugu", false))),
454            (21, Some(("Kannada", true))),
455            (22, Some(("Kannada", false))),
456            (23, Some(("Malayalam", true))),
457            (24, Some(("Malayalam", false))),
458            (25, Some(("Sinhala", true))),
459            (26, Some(("Sinhala", false))),
460            (27, Some(("Thai", true))),
461            (28, Some(("Thai", false))),
462            (29, Some(("Lao", true))),
463            (30, Some(("Lao", false))),
464            (31, Some(("Tibetan", true))),
465            (32, Some(("Tibetan", false))),
466            (33, Some(("Myanmar", true))),
467            (34, Some(("Ethiopic", true))),
468            (35, Some(("Buhid", true))),
469            (36, Some(("Buhid", false))),
470            (37, Some(("Khmer", true))),
471            (38, Some(("Khmer", false))),
472            (39, Some(("Mongolian", true))),
473            (40, Some(("Canadian Syllabics", false))),
474            (41, Some(("Limbu", true))),
475            (42, Some(("Limbu", false))),
476            (43, Some(("Khmer Symbols", false))),
477            (44, Some(("Sundanese", true))),
478            (45, Some(("Ol Chiki", false))),
479            (46, Some(("Georgian (Mkhedruli)", false))),
480            (47, Some(("Sundanese", false))),
481            (48, Some(("Latin Superscript Fallback", false))),
482            (49, Some(("Latin", true))),
483            (50, Some(("Greek", true))),
484            (51, Some(("Greek", false))),
485            (52, Some(("Latin Subscript Fallback", false))),
486            (53, Some(("Coptic", true))),
487            (54, Some(("Coptic", false))),
488            (55, Some(("Georgian (Khutsuri)", false))),
489            (56, Some(("Tifinagh", false))),
490            (57, Some(("Ethiopic", false))),
491            (58, Some(("Cyrillic", true))),
492            (59, Some(("CJKV ideographs", true))),
493            (60, Some(("CJKV ideographs", false))),
494            (61, Some(("Lisu", false))),
495            (62, Some(("Vai", false))),
496            (63, Some(("Cyrillic", true))),
497            (64, Some(("Bamum", true))),
498            (65, Some(("Syloti Nagri", true))),
499            (66, Some(("Syloti Nagri", false))),
500            (67, Some(("Saurashtra", true))),
501            (68, Some(("Saurashtra", false))),
502            (69, Some(("Kayah Li", true))),
503            (70, Some(("Kayah Li", false))),
504            (71, Some(("Myanmar", false))),
505            (72, Some(("Tai Viet", true))),
506            (73, Some(("Tai Viet", false))),
507            (74, Some(("Cherokee", false))),
508            (75, Some(("Armenian", false))),
509            (76, Some(("Hebrew", false))),
510            (77, Some(("Arabic", false))),
511            (78, Some(("Carian", false))),
512            (79, Some(("Gothic", false))),
513            (80, Some(("Deseret", false))),
514            (81, Some(("Shavian", false))),
515            (82, Some(("Osmanya", false))),
516            (83, Some(("Osage", false))),
517            (84, Some(("Cypriot", false))),
518            (85, Some(("Avestan", true))),
519            (86, Some(("Avestan", true))),
520            (87, Some(("Old Turkic", false))),
521            (88, Some(("Hanifi Rohingya", false))),
522            (89, Some(("Chakma", true))),
523            (90, Some(("Chakma", false))),
524            (91, Some(("Mongolian", false))),
525            (92, Some(("CJKV ideographs", false))),
526            (93, Some(("Medefaidrin", false))),
527            (94, Some(("Glagolitic", true))),
528            (95, Some(("Glagolitic", true))),
529            (96, Some(("Adlam", true))),
530            (97, Some(("Adlam", false))),
531        ];
532        check_styles(font_test_data::AUTOHINT_CMAP, ShaperMode::Nominal, expected);
533    }
534
535    #[test]
536    fn shaped_glyph_styles() {
537        // generated by printf debugging in FreeType
538        // (gid, Option<(script_name, is_non_base_char)>)
539        // where "is_non_base_char" more common means "is_mark"
540        let expected = &[
541            (0, Some(("CJKV ideographs", false))),
542            (1, Some(("Latin", false))),
543            (2, Some(("Latin", false))),
544            (3, Some(("Latin", false))),
545            (4, Some(("Latin", false))),
546            // Note: ligatures starting with 'f' are assigned the Cyrillic
547            // script which matches FreeType
548            (5, Some(("Cyrillic", false))),
549            (6, Some(("Cyrillic", false))),
550            (7, Some(("Cyrillic", false))),
551            // Capture the Latin c2sc feature
552            (8, Some(("Latin small capitals from capitals", false))),
553        ];
554        check_styles(
555            font_test_data::NOTOSERIF_AUTOHINT_SHAPING,
556            ShaperMode::BestEffort,
557            expected,
558        );
559    }
560
561    fn check_styles(font_data: &[u8], mode: ShaperMode, expected: &[(u32, Option<(&str, bool)>)]) {
562        let font = FontRef::new(font_data).unwrap();
563        let shaper = Shaper::new(&font, mode);
564        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
565        let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
566        let results = style_map
567            .styles
568            .iter()
569            .enumerate()
570            .map(|(gid, style)| {
571                (
572                    gid as u32,
573                    style
574                        .style_class()
575                        .map(|style_class| (style_class.name, style.is_non_base())),
576                )
577            })
578            .collect::<Vec<_>>();
579        for (i, result) in results.iter().enumerate() {
580            assert_eq!(result, &expected[i]);
581        }
582        // Ensure each style has a remapped metrics index
583        for style in &style_map.styles {
584            style_map.metrics_index(*style).unwrap();
585        }
586    }
587}