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}