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