Skip to main content

skrifa/outline/glyf/hint/
cleartype.rs

1//! ClearType policy and interpreter state.
2
3use raw::{tables::gasp::GaspRangeBehavior, TableProvider};
4
5/// The smoothing and grid fitting a font requests at a given size.
6///
7/// A rasterizer needs both properties. [`Self::symmetric_smoothing`] selects
8/// antialiasing along both axes or only horizontally. [`Self::grid_fit`]
9/// selects whether to hint at all. Windows reads the same two properties from
10/// the same source, so honoring them makes text match Windows.
11///
12/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/gasp>.
13#[derive(Copy, Clone, PartialEq, Eq, Debug)]
14pub struct RasterPolicy {
15    /// True if the rasterizer should smooth along both axes.
16    ///
17    /// When false, the font expects antialiasing only in the horizontal
18    /// direction. A rasterizer that smooths in both directions produces
19    /// softer horizontal edges than the font expects.
20    ///
21    /// This also sets the symmetric rendering bit of the `GETINFO`
22    /// instruction.
23    pub symmetric_smoothing: bool,
24    /// True if TrueType grid fitting should be applied.
25    pub grid_fit: bool,
26}
27
28impl RasterPolicy {
29    /// Returns the policy DirectWrite applies when no version 1 `gasp` range
30    /// covers the size.
31    ///
32    /// This is asymmetric smoothing through 20 ppem for a font that has glyph
33    /// instructions, and symmetric smoothing for everything else.
34    pub(crate) fn fallback(has_glyph_instructions: bool, ppem: u16) -> Self {
35        Self {
36            symmetric_smoothing: !has_glyph_instructions || ppem > 20,
37            grid_fit: has_glyph_instructions,
38        }
39    }
40
41    /// Resolves the policy for the given font at the given nominal ppem.
42    pub(crate) fn resolve<'a>(font: &impl TableProvider<'a>, ppem: u16) -> Self {
43        let has_glyph_instructions = font
44            .selected_maxp()
45            .map(|maxp| maxp.max_size_of_instructions().unwrap_or_default() != 0)
46            .unwrap_or_default();
47        let mut policy = Self::fallback(has_glyph_instructions, ppem);
48        if let Ok(gasp) = font.gasp() {
49            if gasp.version() >= 1 {
50                if let Some(range) = gasp
51                    .gasp_ranges()
52                    .iter()
53                    .find(|range| ppem <= range.range_max_ppem())
54                {
55                    let behavior = range.range_gasp_behavior();
56                    policy.symmetric_smoothing =
57                        behavior.contains(GaspRangeBehavior::GASP_SYMMETRIC_SMOOTHING);
58                    policy.grid_fit = behavior.contains(GaspRangeBehavior::GASP_SYMMETRIC_GRIDFIT);
59                }
60            }
61        }
62        policy
63    }
64}
65
66impl Default for RasterPolicy {
67    fn default() -> Self {
68        Self {
69            symmetric_smoothing: true,
70            grid_fit: true,
71        }
72    }
73}
74
75/// The set of ClearType behaviors the TrueType interpreter applies.
76#[derive(Copy, Clone, PartialEq, Eq, Debug)]
77pub enum ClearType {
78    /// Every glyph is grid fitted, the symmetric rendering bit of `GETINFO`
79    /// follows the hinting options alone, and horizontal moves round to whole
80    /// pixels.
81    Classic,
82    /// A version 1 `gasp` table selects symmetric smoothing and grid fitting
83    /// per size, and glyphs that opt out of backward compatibility keep
84    /// fractional horizontal positions.
85    Enhanced,
86}
87
88/// ClearType configuration for one hinting instance.
89#[derive(Copy, Clone, Debug)]
90pub struct ClearTypeState {
91    /// The set of ClearType behaviors the interpreter applies.
92    pub mode: ClearType,
93    /// True if the font produces broken contours without hinting.
94    ///
95    /// Their glyph programs run without backward compatibility, and the native
96    /// ClearType adjustments are skipped.
97    pub is_hint_reliant: bool,
98    /// The policy resolved from the `gasp` table for this instance's size.
99    pub policy: RasterPolicy,
100}
101
102impl ClearTypeState {
103    /// Creates the state for `mode`.
104    ///
105    /// `is_hint_reliant` and `policy` describe the font at the size.
106    /// [`ClearType::Classic`] ignores the policy and applies the defaults.
107    pub fn new(mode: ClearType, is_hint_reliant: bool, policy: RasterPolicy) -> Self {
108        match mode {
109            ClearType::Classic => Self {
110                is_hint_reliant,
111                ..Self::default()
112            },
113            ClearType::Enhanced => Self {
114                mode,
115                is_hint_reliant,
116                policy,
117            },
118        }
119    }
120
121    /// Returns true if the native ClearType adjustments to the interpreter
122    /// are active.
123    pub fn is_native_enhanced(&self) -> bool {
124        self.mode == ClearType::Enhanced && !self.is_hint_reliant
125    }
126
127    /// Returns the resolved value for the symmetric rendering bit of
128    /// `GETINFO`.
129    pub fn symmetric_smoothing(&self) -> bool {
130        self.mode == ClearType::Classic || self.policy.symmetric_smoothing
131    }
132}
133
134impl Default for ClearTypeState {
135    fn default() -> Self {
136        Self {
137            mode: ClearType::Classic,
138            is_hint_reliant: false,
139            policy: RasterPolicy::default(),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use raw::FontRef;
148
149    // Tinos carries a version 1 gasp with ranges (10, 0xE), (16, 0x7) and
150    // (0xFFFF, 0xF), so symmetric smoothing is off from 11 through 16 ppem.
151    #[test]
152    fn policy_from_gasp_v1() {
153        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
154        for (ppem, symmetric) in [(8, true), (10, true), (11, false), (16, false), (17, true)] {
155            let policy = RasterPolicy::resolve(&font, ppem);
156            assert_eq!(policy.symmetric_smoothing, symmetric, "at {ppem} ppem");
157            assert!(policy.grid_fit, "at {ppem} ppem");
158        }
159    }
160
161    // Every range of Cousine's gasp asks for both properties, so the policy
162    // never differs from the default.
163    #[test]
164    fn policy_from_permissive_gasp() {
165        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
166        for ppem in [8, 12, 20, 40] {
167            let policy = RasterPolicy::resolve(&font, ppem);
168            assert!(policy.symmetric_smoothing);
169            assert!(policy.grid_fit);
170        }
171    }
172
173    // Without a version 1 gasp, DirectWrite smooths asymmetrically through
174    // 20 ppem for a font that carries glyph instructions.
175    #[test]
176    fn fallback_policy() {
177        for (ppem, symmetric) in [(8, false), (20, false), (21, true), (40, true)] {
178            assert_eq!(
179                RasterPolicy::fallback(true, ppem),
180                RasterPolicy {
181                    symmetric_smoothing: symmetric,
182                    grid_fit: true,
183                },
184                "at {ppem} ppem"
185            );
186        }
187        for ppem in [8, 20, 21, 40] {
188            assert_eq!(
189                RasterPolicy::fallback(false, ppem),
190                RasterPolicy {
191                    symmetric_smoothing: true,
192                    grid_fit: false,
193                },
194                "at {ppem} ppem"
195            );
196        }
197    }
198
199    // A font with no glyph instructions is smoothed symmetrically at every
200    // size and is not grid fit.
201    #[test]
202    fn policy_for_uninstructed_font() {
203        let font = FontRef::new(font_test_data::AHEM).unwrap();
204        for ppem in [8, 12, 30] {
205            let policy = RasterPolicy::resolve(&font, ppem);
206            assert!(policy.symmetric_smoothing, "at {ppem} ppem");
207            assert!(!policy.grid_fit, "at {ppem} ppem");
208        }
209    }
210
211    // The policy applies through a font's parsed tables as well as a
212    // FontRef, which is how the scaler reads it.
213    #[test]
214    fn policy_through_the_font_model() {
215        let font = crate::font::Font::new(font_test_data::TINOS_SUBSET.to_vec(), 0).unwrap();
216        let policy = RasterPolicy::resolve(&font.tables(), 12);
217        assert!(!policy.symmetric_smoothing);
218        assert!(policy.grid_fit);
219    }
220
221    #[test]
222    fn classic_state_ignores_the_font() {
223        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
224        let policy = RasterPolicy::resolve(&font, 12);
225        let state = ClearTypeState::new(ClearType::Classic, true, policy);
226        assert!(state.symmetric_smoothing());
227        assert!(!state.is_native_enhanced());
228    }
229
230    #[test]
231    fn enhanced_state_follows_the_gasp() {
232        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
233        let policy = RasterPolicy::resolve(&font, 12);
234        let state = ClearTypeState::new(ClearType::Enhanced, false, policy);
235        assert!(!state.symmetric_smoothing());
236        assert!(state.is_native_enhanced());
237        // A hint reliant font keeps the classic interpreter behavior.
238        let state = ClearTypeState::new(ClearType::Enhanced, true, policy);
239        assert!(!state.is_native_enhanced());
240    }
241}