skrifa/outline/
hint.rs

1//! Support for applying embedded hinting instructions.
2
3use super::{
4    autohint, cff,
5    glyf::{self, FreeTypeScaler},
6    pen::PathStyle,
7    AdjustedMetrics, DrawError, GlyphStyles, Hinting, LocationRef, NormalizedCoord,
8    OutlineCollectionKind, OutlineGlyph, OutlineGlyphCollection, OutlineKind, OutlinePen, Size,
9};
10use crate::alloc::{boxed::Box, vec::Vec};
11use raw::types::Fixed;
12
13/// Configuration settings for a hinting instance.
14#[derive(Clone, Default, Debug)]
15pub struct HintingOptions {
16    /// Specifies the hinting engine to use.
17    ///
18    /// Defaults to [`Engine::AutoFallback`].
19    pub engine: Engine,
20    /// Defines the properties of the intended target of a hinted outline.
21    ///
22    /// Defaults to a target with [`SmoothMode::Normal`] which is equivalent
23    /// to `FT_RENDER_MODE_NORMAL` in FreeType.
24    pub target: Target,
25}
26
27impl From<Target> for HintingOptions {
28    fn from(value: Target) -> Self {
29        Self {
30            engine: Engine::AutoFallback,
31            target: value,
32        }
33    }
34}
35
36/// Specifies the backend to use when applying hints.
37#[derive(Clone, Default, Debug)]
38pub enum Engine {
39    /// The TrueType or PostScript interpreter.
40    Interpreter,
41    /// The automatic hinter that performs just-in-time adjustment of
42    /// outlines.
43    ///
44    /// Glyph styles can be precomputed per font and may be provided here
45    /// as an optimization to avoid recomputing them for each instance.
46    Auto(Option<GlyphStyles>),
47    /// Selects the engine based on the same rules that FreeType uses when
48    /// neither of the `FT_LOAD_NO_AUTOHINT` or `FT_LOAD_FORCE_AUTOHINT`
49    /// load flags are specified.
50    ///
51    /// Specifically, PostScript (CFF/CFF2) fonts will always use the hinting
52    /// engine in the PostScript interpreter and TrueType fonts will use the
53    /// interpreter for TrueType instructions if one of the `fpgm` or `prep`
54    /// tables is non-empty, falling back to the automatic hinter otherwise.
55    ///
56    /// This uses [`OutlineGlyphCollection::prefer_interpreter`] to make a
57    /// selection.
58    #[default]
59    AutoFallback,
60}
61
62impl Engine {
63    /// Converts the `AutoFallback` variant into either `Interpreter` or
64    /// `Auto` based on the given outline set's preference for interpreter
65    /// mode.
66    fn resolve_auto_fallback(self, outlines: &OutlineGlyphCollection) -> Engine {
67        match self {
68            Self::Interpreter => Self::Interpreter,
69            Self::Auto(styles) => Self::Auto(styles),
70            Self::AutoFallback => {
71                if outlines.prefer_interpreter() {
72                    Self::Interpreter
73                } else {
74                    Self::Auto(None)
75                }
76            }
77        }
78    }
79}
80
81impl From<Engine> for HintingOptions {
82    fn from(value: Engine) -> Self {
83        Self {
84            engine: value,
85            target: Default::default(),
86        }
87    }
88}
89
90/// Defines the target settings for hinting.
91#[derive(Copy, Clone, PartialEq, Eq, Debug)]
92pub enum Target {
93    /// Strong hinting style that should only be used for aliased, monochromatic
94    /// rasterization.
95    ///
96    /// Corresponds to `FT_LOAD_TARGET_MONO` in FreeType.
97    Mono,
98    /// Hinting style that is suitable for anti-aliased rasterization.
99    ///
100    /// Corresponds to the non-monochrome load targets in FreeType. See
101    /// [`SmoothMode`] for more detail.
102    Smooth {
103        /// The basic mode for smooth hinting.
104        ///
105        /// Defaults to [`SmoothMode::Normal`].
106        mode: SmoothMode,
107        /// If true, TrueType bytecode may assume that the resulting outline
108        /// will be rasterized with supersampling in the vertical direction.
109        ///
110        /// When this is enabled, ClearType fonts will often generate wider
111        /// horizontal stems that may lead to blurry images when rendered with
112        /// an analytical area rasterizer (such as the one in FreeType).
113        ///
114        /// The effect of this setting is to control the "ClearType symmetric
115        /// rendering bit" of the TrueType `GETINFO` instruction. For more
116        /// detail, see this [issue](https://github.com/googlefonts/fontations/issues/1080).
117        ///
118        /// FreeType has no corresponding setting and behaves as if this is
119        /// always enabled.
120        ///
121        /// This only applies to the TrueType interpreter.
122        ///
123        /// Defaults to `true`.
124        symmetric_rendering: bool,
125        /// If true, prevents adjustment of the outline in the horizontal
126        /// direction and preserves inter-glyph spacing.
127        ///
128        /// This is useful for performing layout without concern that hinting
129        /// will modify the advance width of a glyph. Specifically, it means
130        /// that layout will not require evaluation of glyph outlines.
131        ///
132        /// FreeType has no corresponding setting and behaves as if this is
133        /// always disabled.
134        ///
135        /// This applies to the TrueType interpreter and the automatic hinter.
136        ///
137        /// Defaults to `false`.       
138        preserve_linear_metrics: bool,
139    },
140}
141
142impl Default for Target {
143    fn default() -> Self {
144        SmoothMode::Normal.into()
145    }
146}
147
148/// Mode selector for a smooth hinting target.
149#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
150pub enum SmoothMode {
151    /// The standard smooth hinting mode.
152    ///
153    /// Corresponds to `FT_LOAD_TARGET_NORMAL` in FreeType.
154    #[default]
155    Normal,
156    /// Hinting with a lighter touch, typically meaning less aggressive
157    /// adjustment in the horizontal direction.
158    ///
159    /// Corresponds to `FT_LOAD_TARGET_LIGHT` in FreeType.
160    Light,
161    /// Hinting that is optimized for subpixel rendering with horizontal LCD
162    /// layouts.
163    ///
164    /// Corresponds to `FT_LOAD_TARGET_LCD` in FreeType.
165    Lcd,
166    /// Hinting that is optimized for subpixel rendering with vertical LCD
167    /// layouts.
168    ///
169    /// Corresponds to `FT_LOAD_TARGET_LCD_V` in FreeType.
170    VerticalLcd,
171}
172
173impl From<SmoothMode> for Target {
174    fn from(value: SmoothMode) -> Self {
175        Self::Smooth {
176            mode: value,
177            symmetric_rendering: true,
178            preserve_linear_metrics: false,
179        }
180    }
181}
182
183/// Modes that control hinting when using embedded instructions.
184///
185/// Only the TrueType interpreter supports all hinting modes.
186///
187/// # FreeType compatibility
188///
189/// The following table describes how to map FreeType hinting modes:
190///
191/// | FreeType mode         | Variant                                                                              |
192/// |-----------------------|--------------------------------------------------------------------------------------|
193/// | FT_LOAD_TARGET_MONO   | Strong                                                                               |
194/// | FT_LOAD_TARGET_NORMAL | Smooth { lcd_subpixel: None, preserve_linear_metrics: false }                        |
195/// | FT_LOAD_TARGET_LCD    | Smooth { lcd_subpixel: Some(LcdLayout::Horizontal), preserve_linear_metrics: false } |
196/// | FT_LOAD_TARGET_LCD_V  | Smooth { lcd_subpixel: Some(LcdLayout::Vertical), preserve_linear_metrics: false }   |
197///
198/// Note: `FT_LOAD_TARGET_LIGHT` is equivalent to `FT_LOAD_TARGET_NORMAL` since
199/// FreeType 2.7.
200///
201/// The default value of this type is equivalent to `FT_LOAD_TARGET_NORMAL`.
202#[doc(hidden)]
203#[derive(Copy, Clone, PartialEq, Eq, Debug)]
204pub enum HintingMode {
205    /// Strong hinting mode that should only be used for aliased, monochromatic
206    /// rasterization.
207    ///
208    /// Corresponds to `FT_LOAD_TARGET_MONO` in FreeType.
209    Strong,
210    /// Lighter hinting mode that is intended for anti-aliased rasterization.
211    Smooth {
212        /// If set, enables support for optimized hinting that takes advantage
213        /// of subpixel layouts in LCD displays and corresponds to
214        /// `FT_LOAD_TARGET_LCD` or `FT_LOAD_TARGET_LCD_V` in FreeType.
215        ///
216        /// If unset, corresponds to `FT_LOAD_TARGET_NORMAL` in FreeType.
217        lcd_subpixel: Option<LcdLayout>,
218        /// If true, prevents adjustment of the outline in the horizontal
219        /// direction and preserves inter-glyph spacing.
220        ///
221        /// This is useful for performing layout without concern that hinting
222        /// will modify the advance width of a glyph. Specifically, it means
223        /// that layout will not require evaluation of glyph outlines.
224        ///
225        /// FreeType has no corresponding setting.
226        preserve_linear_metrics: bool,
227    },
228}
229
230impl Default for HintingMode {
231    fn default() -> Self {
232        Self::Smooth {
233            lcd_subpixel: None,
234            preserve_linear_metrics: false,
235        }
236    }
237}
238
239impl From<HintingMode> for HintingOptions {
240    fn from(value: HintingMode) -> Self {
241        let target = match value {
242            HintingMode::Strong => Target::Mono,
243            HintingMode::Smooth {
244                lcd_subpixel,
245                preserve_linear_metrics,
246            } => {
247                let mode = match lcd_subpixel {
248                    Some(LcdLayout::Horizontal) => SmoothMode::Lcd,
249                    Some(LcdLayout::Vertical) => SmoothMode::VerticalLcd,
250                    None => SmoothMode::Normal,
251                };
252                Target::Smooth {
253                    mode,
254                    preserve_linear_metrics,
255                    symmetric_rendering: true,
256                }
257            }
258        };
259        target.into()
260    }
261}
262
263/// Specifies direction of pixel layout for LCD based subpixel hinting.
264#[doc(hidden)]
265#[derive(Copy, Clone, PartialEq, Eq, Debug)]
266pub enum LcdLayout {
267    /// Subpixels are ordered horizontally.
268    ///
269    /// Corresponds to `FT_LOAD_TARGET_LCD` in FreeType.
270    Horizontal,
271    /// Subpixels are ordered vertically.
272    ///
273    /// Corresponds to `FT_LOAD_TARGET_LCD_V` in FreeType.
274    Vertical,
275}
276
277/// Hinting instance that uses information embedded in the font to perform
278/// grid-fitting.
279#[derive(Clone)]
280pub struct HintingInstance {
281    size: Size,
282    coords: Vec<NormalizedCoord>,
283    target: Target,
284    kind: HinterKind,
285}
286
287impl HintingInstance {
288    /// Creates a new embedded hinting instance for the given outline
289    /// collection, size, location in variation space and hinting mode.
290    pub fn new<'a>(
291        outline_glyphs: &OutlineGlyphCollection,
292        size: Size,
293        location: impl Into<LocationRef<'a>>,
294        options: impl Into<HintingOptions>,
295    ) -> Result<Self, DrawError> {
296        let options = options.into();
297        let mut hinter = Self {
298            size: Size::unscaled(),
299            coords: vec![],
300            target: options.target,
301            kind: HinterKind::None,
302        };
303        hinter.reconfigure(outline_glyphs, size, location, options)?;
304        Ok(hinter)
305    }
306
307    /// Returns the currently configured size.
308    pub fn size(&self) -> Size {
309        self.size
310    }
311
312    /// Returns the currently configured normalized location in variation space.
313    pub fn location(&self) -> LocationRef<'_> {
314        LocationRef::new(&self.coords)
315    }
316
317    /// Returns the currently configured hinting target.
318    pub fn target(&self) -> Target {
319        self.target
320    }
321
322    /// Resets the hinter state for a new font instance with the given
323    /// outline collection and settings.
324    pub fn reconfigure<'a>(
325        &mut self,
326        outlines: &OutlineGlyphCollection,
327        size: Size,
328        location: impl Into<LocationRef<'a>>,
329        options: impl Into<HintingOptions>,
330    ) -> Result<(), DrawError> {
331        self.size = size;
332        self.coords.clear();
333        self.coords
334            .extend_from_slice(location.into().effective_coords());
335        let options = options.into();
336        self.target = options.target;
337        let engine = options.engine.resolve_auto_fallback(outlines);
338        // Reuse memory if the font contains the same outline format
339        let current_kind = core::mem::replace(&mut self.kind, HinterKind::None);
340        match engine {
341            Engine::Interpreter => match &outlines.kind {
342                OutlineCollectionKind::Glyf(glyf) => {
343                    let mut hint_instance = match current_kind {
344                        HinterKind::Glyf(instance) => instance,
345                        _ => Box::<glyf::HintInstance>::default(),
346                    };
347                    let ppem = size.ppem();
348                    let scale = glyf.compute_hinted_scale(ppem).1.to_bits();
349                    // Use fixed point rounding for ppem to match what FreeType does:
350                    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L3349>
351                    // issue: <https://github.com/googlefonts/fontations/issues/1544>
352                    let rounded_ppem = ((Fixed::from_bits(scale)
353                        * Fixed::from_bits(glyf.units_per_em() as i32))
354                    .to_bits()
355                        + 32)
356                        >> 6;
357                    hint_instance.reconfigure(
358                        glyf,
359                        scale,
360                        rounded_ppem,
361                        self.target,
362                        &self.coords,
363                    )?;
364                    self.kind = HinterKind::Glyf(hint_instance);
365                }
366                OutlineCollectionKind::Cff(cff) => {
367                    let mut subfonts = match current_kind {
368                        HinterKind::Cff(subfonts) => subfonts,
369                        _ => vec![],
370                    };
371                    subfonts.clear();
372                    let ppem = size.ppem();
373                    for i in 0..cff.subfont_count() {
374                        subfonts.push(cff.subfont(i, ppem, &self.coords)?);
375                    }
376                    self.kind = HinterKind::Cff(subfonts);
377                }
378                OutlineCollectionKind::None => {}
379            },
380            Engine::Auto(styles) => {
381                let Some(font) = outlines.font() else {
382                    return Ok(());
383                };
384                let instance = autohint::Instance::new(
385                    font,
386                    outlines,
387                    &self.coords,
388                    self.target,
389                    styles,
390                    true,
391                );
392                self.kind = HinterKind::Auto(instance);
393            }
394            _ => {}
395        }
396        Ok(())
397    }
398
399    /// Returns true if hinting should actually be applied for this instance.
400    ///
401    /// Some TrueType fonts disable hinting dynamically based on the instance
402    /// configuration.
403    pub fn is_enabled(&self) -> bool {
404        match &self.kind {
405            HinterKind::Glyf(instance) => instance.is_enabled(),
406            HinterKind::Cff(_) | HinterKind::Auto(_) => true,
407            _ => false,
408        }
409    }
410
411    pub(super) fn draw(
412        &self,
413        glyph: &OutlineGlyph,
414        memory: Option<&mut [u8]>,
415        path_style: PathStyle,
416        pen: &mut impl OutlinePen,
417        is_pedantic: bool,
418    ) -> Result<AdjustedMetrics, DrawError> {
419        let ppem = self.size.ppem();
420        let coords = self.coords.as_slice();
421        match (&self.kind, &glyph.kind) {
422            (HinterKind::Auto(instance), _) => {
423                instance.draw(self.size, coords, glyph, path_style, pen)
424            }
425            (HinterKind::Glyf(instance), OutlineKind::Glyf(glyf, outline)) => {
426                if matches!(path_style, PathStyle::HarfBuzz) {
427                    return Err(DrawError::HarfBuzzHintingUnsupported);
428                }
429                super::with_glyf_memory(outline, Hinting::Embedded, memory, |buf| {
430                    let scaled_outline = FreeTypeScaler::hinted(
431                        glyf,
432                        outline,
433                        buf,
434                        ppem,
435                        coords,
436                        instance,
437                        is_pedantic,
438                    )?
439                    .scale(&outline.glyph, outline.glyph_id)?;
440                    scaled_outline.to_path(path_style, pen)?;
441                    Ok(AdjustedMetrics {
442                        has_overlaps: outline.has_overlaps,
443                        lsb: Some(scaled_outline.adjusted_lsb().to_f32()),
444                        // When hinting is requested, we round the advance
445                        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L889>
446                        advance_width: Some(
447                            scaled_outline.adjusted_advance_width().round().to_f32(),
448                        ),
449                    })
450                })
451            }
452            (HinterKind::Cff(subfonts), OutlineKind::Cff(cff, glyph_id, subfont_ix)) => {
453                let Some(subfont) = subfonts.get(*subfont_ix as usize) else {
454                    return Err(DrawError::NoSources);
455                };
456                cff.draw(subfont, *glyph_id, &self.coords, true, pen)?;
457                Ok(AdjustedMetrics::default())
458            }
459            _ => Err(DrawError::NoSources),
460        }
461    }
462}
463
464#[derive(Clone)]
465enum HinterKind {
466    /// Represents a hinting instance that is associated with an empty outline
467    /// collection.
468    None,
469    Glyf(Box<glyf::HintInstance>),
470    Cff(Vec<cff::Subfont>),
471    Auto(autohint::Instance),
472}
473
474// Internal helpers for deriving various flags from the mode which
475// change the behavior of certain instructions.
476// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L2222>
477impl Target {
478    pub(crate) fn is_smooth(&self) -> bool {
479        matches!(self, Self::Smooth { .. })
480    }
481
482    pub(crate) fn is_grayscale_cleartype(&self) -> bool {
483        match self {
484            Self::Smooth { mode, .. } => matches!(mode, SmoothMode::Normal | SmoothMode::Light),
485            _ => false,
486        }
487    }
488
489    pub(crate) fn is_light(&self) -> bool {
490        matches!(
491            self,
492            Self::Smooth {
493                mode: SmoothMode::Light,
494                ..
495            }
496        )
497    }
498
499    pub(crate) fn is_lcd(&self) -> bool {
500        matches!(
501            self,
502            Self::Smooth {
503                mode: SmoothMode::Lcd,
504                ..
505            }
506        )
507    }
508
509    pub(crate) fn is_vertical_lcd(&self) -> bool {
510        matches!(
511            self,
512            Self::Smooth {
513                mode: SmoothMode::VerticalLcd,
514                ..
515            }
516        )
517    }
518
519    pub(crate) fn symmetric_rendering(&self) -> bool {
520        matches!(
521            self,
522            Self::Smooth {
523                symmetric_rendering: true,
524                ..
525            }
526        )
527    }
528
529    pub(crate) fn preserve_linear_metrics(&self) -> bool {
530        matches!(
531            self,
532            Self::Smooth {
533                preserve_linear_metrics: true,
534                ..
535            }
536        )
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::{
544        outline::{
545            pen::{NullPen, SvgPen},
546            DrawSettings,
547        },
548        raw::TableProvider,
549        FontRef, GlyphId, MetadataProvider,
550    };
551
552    // FreeType ignores the hdmx table when backward compatibility mode
553    // is enabled in the TrueType interpreter.
554    #[test]
555    fn ignore_hdmx_when_back_compat_enabled() {
556        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
557        let outlines = font.outline_glyphs();
558        // Double quote was the most egregious failure
559        let gid = font.charmap().map('"').unwrap();
560        let font_size = 16;
561        let hinter = HintingInstance::new(
562            &outlines,
563            Size::new(font_size as f32),
564            LocationRef::default(),
565            HintingOptions::default(),
566        )
567        .unwrap();
568        let HinterKind::Glyf(tt_hinter) = &hinter.kind else {
569            panic!("this is definitely a TrueType hinter");
570        };
571        // Make sure backward compatibility mode is enabled
572        assert!(tt_hinter.backward_compatibility());
573        let outline = outlines.get(gid).unwrap();
574        let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
575        // FreeType computes an advance width of 7 when hinting but hdmx contains 5
576        let scaler_advance = metrics.advance_width.unwrap();
577        assert_eq!(scaler_advance, 7.0);
578        let hdmx_advance = font
579            .hdmx()
580            .unwrap()
581            .record_for_size(font_size)
582            .unwrap()
583            .widths()[gid.to_u32() as usize];
584        assert_eq!(hdmx_advance, 5);
585    }
586
587    // When hinting is disabled by the prep table, FreeType still returns
588    // rounded advance widths
589    #[test]
590    fn round_advance_when_prep_disables_hinting() {
591        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
592        let outlines = font.outline_glyphs();
593        let gid = font.charmap().map('"').unwrap();
594        let size = Size::new(16.0);
595        let location = LocationRef::default();
596        let mut hinter =
597            HintingInstance::new(&outlines, size, location, HintingOptions::default()).unwrap();
598        let HinterKind::Glyf(tt_hinter) = &mut hinter.kind else {
599            panic!("this is definitely a TrueType hinter");
600        };
601        tt_hinter.simulate_prep_flag_suppress_hinting();
602        let outline = outlines.get(gid).unwrap();
603        // And we still have a rounded advance
604        let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
605        assert_eq!(metrics.advance_width, Some(7.0));
606        // Unhinted advance has some fractional bits
607        let metrics = outline
608            .draw(DrawSettings::unhinted(size, location), &mut NullPen)
609            .unwrap();
610        assert_eq!(metrics.advance_width, Some(6.53125));
611    }
612
613    // Check that we round the value for the MPPEM instruction when applying
614    // a fractional font size
615    // <https://github.com/googlefonts/fontations/issues/1544>
616    #[test]
617    fn hint_fractional_font_size() {
618        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
619        let outlines = font.outline_glyphs();
620        let gid = GlyphId::new(1); // was 85 in the original font
621        let size = Size::new(24.8);
622        let location = LocationRef::default();
623        let hinter =
624            HintingInstance::new(&outlines, size, location, HintingOptions::default()).unwrap();
625        let outline = outlines.get(gid).unwrap();
626        let mut pen = SvgPen::new();
627        outline.draw(&hinter, &mut pen).unwrap();
628        assert_eq!(
629            pen.to_string(),
630            "M12.65625,11.015625 Q11.296875,11.421875 10.078125,11.421875 Q8.140625,11.421875 6.9375,9.9375 Q5.734375,8.46875 5.734375,6.1875 L5.734375,0 L3.5625,0 L3.5625,8.421875 Q3.5625,9.328125 3.390625,10.5625 Q3.234375,11.8125 2.9375,13 L5,13 Q5.484375,11.34375 5.578125,10 L5.640625,10 Q6.25,11.25 6.828125,11.828125 Q7.40625,12.40625 8.203125,12.703125 Q9.015625,13 10.15625,13 Q11.421875,13 12.65625,13 Z"
631        );
632    }
633}