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            read_fonts::mem::with_scratch(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        shaper.context().for_each_mapping(&mut |ch, gid| {
175            let Some(style) = map.styles.get_mut(gid.to_u32() as usize) else {
176                return;
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                    return;
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                return;
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                .context()
247                .map(digit_char as u32)
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::outline::autohint::context::FontRefContext;
417    use crate::{raw::TableProvider, FontRef, MetadataProvider};
418
419    /// Ensure that style mapping accurately applies the DIGIT bit to
420    /// ASCII digit glyphs.
421    #[test]
422    fn capture_digit_styles() {
423        let font = FontRef::new(font_test_data::AHEM).unwrap();
424        let context = FontRefContext::new(&font, &[]);
425        let shaper = Shaper::new(&context, ShaperMode::Nominal);
426        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
427        let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
428        let charmap = font.charmap();
429        let mut digit_count = 0;
430        for (ch, gid) in charmap.mappings() {
431            let style = style_map.style(gid).unwrap();
432            let is_char_digit = char::from_u32(ch).unwrap().is_ascii_digit();
433            assert_eq!(style.is_digit(), is_char_digit);
434            digit_count += is_char_digit as u32;
435        }
436        // This font has all 10 ASCII digits
437        assert_eq!(digit_count, 10);
438    }
439
440    #[test]
441    fn glyph_styles() {
442        // generated by printf debugging in FreeType
443        // (gid, Option<(script_name, is_non_base_char)>)
444        // where "is_non_base_char" more common means "is_mark"
445        let expected = &[
446            (0, Some(("CJKV ideographs", false))),
447            (1, Some(("Latin", true))),
448            (2, Some(("Armenian", true))),
449            (3, Some(("Hebrew", true))),
450            (4, Some(("Arabic", false))),
451            (5, Some(("Arabic", false))),
452            (6, Some(("Arabic", true))),
453            (7, Some(("Devanagari", true))),
454            (8, Some(("Devanagari", false))),
455            (9, Some(("Bengali", true))),
456            (10, Some(("Bengali", false))),
457            (11, Some(("Gurmukhi", true))),
458            (12, Some(("Gurmukhi", false))),
459            (13, Some(("Gujarati", true))),
460            (14, Some(("Gujarati", true))),
461            (15, Some(("Oriya", true))),
462            (16, Some(("Oriya", false))),
463            (17, Some(("Tamil", true))),
464            (18, Some(("Tamil", false))),
465            (19, Some(("Telugu", true))),
466            (20, Some(("Telugu", false))),
467            (21, Some(("Kannada", true))),
468            (22, Some(("Kannada", false))),
469            (23, Some(("Malayalam", true))),
470            (24, Some(("Malayalam", false))),
471            (25, Some(("Sinhala", true))),
472            (26, Some(("Sinhala", false))),
473            (27, Some(("Thai", true))),
474            (28, Some(("Thai", false))),
475            (29, Some(("Lao", true))),
476            (30, Some(("Lao", false))),
477            (31, Some(("Tibetan", true))),
478            (32, Some(("Tibetan", false))),
479            (33, Some(("Myanmar", true))),
480            (34, Some(("Ethiopic", true))),
481            (35, Some(("Buhid", true))),
482            (36, Some(("Buhid", false))),
483            (37, Some(("Khmer", true))),
484            (38, Some(("Khmer", false))),
485            (39, Some(("Mongolian", true))),
486            (40, Some(("Canadian Syllabics", false))),
487            (41, Some(("Limbu", true))),
488            (42, Some(("Limbu", false))),
489            (43, Some(("Khmer Symbols", false))),
490            (44, Some(("Sundanese", true))),
491            (45, Some(("Ol Chiki", false))),
492            (46, Some(("Georgian (Mkhedruli)", false))),
493            (47, Some(("Sundanese", false))),
494            (48, Some(("Latin Superscript Fallback", false))),
495            (49, Some(("Latin", true))),
496            (50, Some(("Greek", true))),
497            (51, Some(("Greek", false))),
498            (52, Some(("Latin Subscript Fallback", false))),
499            (53, Some(("Coptic", true))),
500            (54, Some(("Coptic", false))),
501            (55, Some(("Georgian (Khutsuri)", false))),
502            (56, Some(("Tifinagh", false))),
503            (57, Some(("Ethiopic", false))),
504            (58, Some(("Cyrillic", true))),
505            (59, Some(("CJKV ideographs", true))),
506            (60, Some(("CJKV ideographs", false))),
507            (61, Some(("Lisu", false))),
508            (62, Some(("Vai", false))),
509            (63, Some(("Cyrillic", true))),
510            (64, Some(("Bamum", true))),
511            (65, Some(("Syloti Nagri", true))),
512            (66, Some(("Syloti Nagri", false))),
513            (67, Some(("Saurashtra", true))),
514            (68, Some(("Saurashtra", false))),
515            (69, Some(("Kayah Li", true))),
516            (70, Some(("Kayah Li", false))),
517            (71, Some(("Myanmar", false))),
518            (72, Some(("Tai Viet", true))),
519            (73, Some(("Tai Viet", false))),
520            (74, Some(("Cherokee", false))),
521            (75, Some(("Armenian", false))),
522            (76, Some(("Hebrew", false))),
523            (77, Some(("Arabic", false))),
524            (78, Some(("Carian", false))),
525            (79, Some(("Gothic", false))),
526            (80, Some(("Deseret", false))),
527            (81, Some(("Shavian", false))),
528            (82, Some(("Osmanya", false))),
529            (83, Some(("Osage", false))),
530            (84, Some(("Cypriot", false))),
531            (85, Some(("Avestan", true))),
532            (86, Some(("Avestan", true))),
533            (87, Some(("Old Turkic", false))),
534            (88, Some(("Hanifi Rohingya", false))),
535            (89, Some(("Chakma", true))),
536            (90, Some(("Chakma", false))),
537            (91, Some(("Mongolian", false))),
538            (92, Some(("CJKV ideographs", false))),
539            (93, Some(("Medefaidrin", false))),
540            (94, Some(("Glagolitic", true))),
541            (95, Some(("Glagolitic", true))),
542            (96, Some(("Adlam", true))),
543            (97, Some(("Adlam", false))),
544        ];
545        check_styles(font_test_data::AUTOHINT_CMAP, ShaperMode::Nominal, expected);
546    }
547
548    #[test]
549    fn shaped_glyph_styles() {
550        // generated by printf debugging in FreeType
551        // (gid, Option<(script_name, is_non_base_char)>)
552        // where "is_non_base_char" more common means "is_mark"
553        let expected = &[
554            (0, Some(("CJKV ideographs", false))),
555            (1, Some(("Latin", false))),
556            (2, Some(("Latin", false))),
557            (3, Some(("Latin", false))),
558            (4, Some(("Latin", false))),
559            // Note: ligatures starting with 'f' are assigned the Cyrillic
560            // script which matches FreeType
561            (5, Some(("Cyrillic", false))),
562            (6, Some(("Cyrillic", false))),
563            (7, Some(("Cyrillic", false))),
564            // Capture the Latin c2sc feature
565            (8, Some(("Latin small capitals from capitals", false))),
566        ];
567        check_styles(
568            font_test_data::NOTOSERIF_AUTOHINT_SHAPING,
569            ShaperMode::BestEffort,
570            expected,
571        );
572    }
573
574    fn check_styles(font_data: &[u8], mode: ShaperMode, expected: &[(u32, Option<(&str, bool)>)]) {
575        let font = FontRef::new(font_data).unwrap();
576        let context = FontRefContext::new(&font, &[]);
577        let shaper = Shaper::new(&context, mode);
578        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
579        let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
580        let results = style_map
581            .styles
582            .iter()
583            .enumerate()
584            .map(|(gid, style)| {
585                (
586                    gid as u32,
587                    style
588                        .style_class()
589                        .map(|style_class| (style_class.name, style.is_non_base())),
590                )
591            })
592            .collect::<Vec<_>>();
593        for (i, result) in results.iter().enumerate() {
594            assert_eq!(result, &expected[i]);
595        }
596        // Ensure each style has a remapped metrics index
597        for style in &style_map.styles {
598            style_map.metrics_index(*style).unwrap();
599        }
600    }
601}