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