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}