Skip to main content

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