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::Varc(..) => {
379 self.kind = HinterKind::Varc;
380 }
381 OutlineCollectionKind::None => {}
382 },
383 Engine::Auto(styles) => {
384 let Some(font) = outlines.font() else {
385 return Ok(());
386 };
387 let instance = autohint::Instance::new(
388 font,
389 outlines,
390 &self.coords,
391 self.target,
392 styles,
393 true,
394 );
395 self.kind = HinterKind::Auto(instance);
396 }
397 _ => {}
398 }
399 Ok(())
400 }
401
402 /// Returns true if hinting should actually be applied for this instance.
403 ///
404 /// Some TrueType fonts disable hinting dynamically based on the instance
405 /// configuration.
406 pub fn is_enabled(&self) -> bool {
407 match &self.kind {
408 HinterKind::Glyf(instance) => instance.is_enabled(),
409 HinterKind::Cff(_) | HinterKind::Auto(_) | HinterKind::Varc => true,
410 _ => false,
411 }
412 }
413
414 pub(super) fn draw(
415 &self,
416 glyph: &OutlineGlyph,
417 memory: Option<&mut [u8]>,
418 path_style: PathStyle,
419 pen: &mut impl OutlinePen,
420 is_pedantic: bool,
421 ) -> Result<AdjustedMetrics, DrawError> {
422 let ppem = self.size.ppem();
423 let coords = self.coords.as_slice();
424 match (&self.kind, &glyph.kind) {
425 (HinterKind::Auto(instance), _) => {
426 instance.draw(self.size, coords, glyph, path_style, pen)
427 }
428 (HinterKind::Glyf(instance), OutlineKind::Glyf(glyf, outline)) => {
429 if matches!(path_style, PathStyle::HarfBuzz) {
430 return Err(DrawError::HarfBuzzHintingUnsupported);
431 }
432 super::with_temporary_memory(glyph, Hinting::Embedded, memory, |buf| {
433 let scaled_outline = FreeTypeScaler::hinted(
434 glyf,
435 outline,
436 buf,
437 ppem,
438 coords,
439 instance,
440 is_pedantic,
441 )?
442 .scale(&outline.glyph, outline.glyph_id)?;
443 scaled_outline.to_path(path_style, pen)?;
444 Ok(AdjustedMetrics {
445 has_overlaps: outline.has_overlaps,
446 lsb: Some(scaled_outline.adjusted_lsb().to_f32()),
447 // When hinting is requested, we round the advance
448 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L889>
449 advance_width: Some(
450 scaled_outline.adjusted_advance_width().round().to_f32(),
451 ),
452 })
453 })
454 }
455 (HinterKind::Cff(subfonts), OutlineKind::Cff(cff, glyph_id, subfont_ix)) => {
456 let Some(subfont) = subfonts.get(*subfont_ix as usize) else {
457 return Err(DrawError::NoSources);
458 };
459 cff.draw(subfont, *glyph_id, &self.coords, true, pen)?;
460 Ok(AdjustedMetrics::default())
461 }
462 (HinterKind::Varc, OutlineKind::Varc(varc, outline)) => {
463 super::with_temporary_memory(glyph, Hinting::None, memory, |buf| {
464 varc.draw(outline, buf, self.size, &self.coords, path_style, pen)?;
465 Ok(AdjustedMetrics::default())
466 })
467 }
468 _ => Err(DrawError::NoSources),
469 }
470 }
471}
472
473#[derive(Clone)]
474enum HinterKind {
475 /// Represents a hinting instance that is associated with an empty outline
476 /// collection.
477 None,
478 Glyf(Box<glyf::HintInstance>),
479 Cff(Vec<cff::Subfont>),
480 Varc,
481 Auto(autohint::Instance),
482}
483
484// Internal helpers for deriving various flags from the mode which
485// change the behavior of certain instructions.
486// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L2222>
487impl Target {
488 pub(crate) fn is_smooth(&self) -> bool {
489 matches!(self, Self::Smooth { .. })
490 }
491
492 pub(crate) fn is_grayscale_cleartype(&self) -> bool {
493 match self {
494 Self::Smooth { mode, .. } => matches!(mode, SmoothMode::Normal | SmoothMode::Light),
495 _ => false,
496 }
497 }
498
499 pub(crate) fn is_light(&self) -> bool {
500 matches!(
501 self,
502 Self::Smooth {
503 mode: SmoothMode::Light,
504 ..
505 }
506 )
507 }
508
509 pub(crate) fn is_lcd(&self) -> bool {
510 matches!(
511 self,
512 Self::Smooth {
513 mode: SmoothMode::Lcd,
514 ..
515 }
516 )
517 }
518
519 pub(crate) fn is_vertical_lcd(&self) -> bool {
520 matches!(
521 self,
522 Self::Smooth {
523 mode: SmoothMode::VerticalLcd,
524 ..
525 }
526 )
527 }
528
529 pub(crate) fn symmetric_rendering(&self) -> bool {
530 matches!(
531 self,
532 Self::Smooth {
533 symmetric_rendering: true,
534 ..
535 }
536 )
537 }
538
539 pub(crate) fn preserve_linear_metrics(&self) -> bool {
540 matches!(
541 self,
542 Self::Smooth {
543 preserve_linear_metrics: true,
544 ..
545 }
546 )
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::{
554 outline::{
555 pen::{NullPen, SvgPen},
556 DrawSettings,
557 },
558 raw::TableProvider,
559 FontRef, GlyphId, MetadataProvider,
560 };
561
562 // FreeType ignores the hdmx table when backward compatibility mode
563 // is enabled in the TrueType interpreter.
564 #[test]
565 fn ignore_hdmx_when_back_compat_enabled() {
566 let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
567 let outlines = font.outline_glyphs();
568 // Double quote was the most egregious failure
569 let gid = font.charmap().map('"').unwrap();
570 let font_size = 16;
571 let hinter = HintingInstance::new(
572 &outlines,
573 Size::new(font_size as f32),
574 LocationRef::default(),
575 HintingOptions::default(),
576 )
577 .unwrap();
578 let HinterKind::Glyf(tt_hinter) = &hinter.kind else {
579 panic!("this is definitely a TrueType hinter");
580 };
581 // Make sure backward compatibility mode is enabled
582 assert!(tt_hinter.backward_compatibility());
583 let outline = outlines.get(gid).unwrap();
584 let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
585 // FreeType computes an advance width of 7 when hinting but hdmx contains 5
586 let scaler_advance = metrics.advance_width.unwrap();
587 assert_eq!(scaler_advance, 7.0);
588 let hdmx_advance = font
589 .hdmx()
590 .unwrap()
591 .record_for_size(font_size)
592 .unwrap()
593 .widths()[gid.to_u32() as usize];
594 assert_eq!(hdmx_advance, 5);
595 }
596
597 // When hinting is disabled by the prep table, FreeType still returns
598 // rounded advance widths
599 #[test]
600 fn round_advance_when_prep_disables_hinting() {
601 let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
602 let outlines = font.outline_glyphs();
603 let gid = font.charmap().map('"').unwrap();
604 let size = Size::new(16.0);
605 let location = LocationRef::default();
606 let mut hinter =
607 HintingInstance::new(&outlines, size, location, HintingOptions::default()).unwrap();
608 let HinterKind::Glyf(tt_hinter) = &mut hinter.kind else {
609 panic!("this is definitely a TrueType hinter");
610 };
611 tt_hinter.simulate_prep_flag_suppress_hinting();
612 let outline = outlines.get(gid).unwrap();
613 // And we still have a rounded advance
614 let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
615 assert_eq!(metrics.advance_width, Some(7.0));
616 // Unhinted advance has some fractional bits
617 let metrics = outline
618 .draw(DrawSettings::unhinted(size, location), &mut NullPen)
619 .unwrap();
620 assert_eq!(metrics.advance_width, Some(6.53125));
621 }
622
623 // Check that we round the value for the MPPEM instruction when applying
624 // a fractional font size
625 // <https://github.com/googlefonts/fontations/issues/1544>
626 #[test]
627 fn hint_fractional_font_size() {
628 let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
629 let outlines = font.outline_glyphs();
630 let gid = GlyphId::new(1); // was 85 in the original font
631 let size = Size::new(24.8);
632 let location = LocationRef::default();
633 let hinter =
634 HintingInstance::new(&outlines, size, location, HintingOptions::default()).unwrap();
635 let outline = outlines.get(gid).unwrap();
636 let mut pen = SvgPen::new();
637 outline.draw(&hinter, &mut pen).unwrap();
638 assert_eq!(
639 pen.to_string(),
640 "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"
641 );
642 }
643}