Skip to main content

read_fonts/tables/glyf/outline/
hinter.rs

1//! The hook for a TrueType interpreter.
2
3use crate::tables::glyf::{bytecode::HintError, PointCoord, PointFlags, PHANTOM_POINT_COUNT};
4use crate::types::{F2Dot14, GlyphId, Point};
5
6/// Receives each instructed glyph from
7/// [`OutlinePlan::load`](super::OutlinePlan::load).
8pub trait Hinter<C: PointCoord> {
9    /// Whether backward compatibility mode is active, which affects phantom
10    /// point rounding even for a glyph with no instructions.
11    fn backward_compatibility(&self) -> bool {
12        true
13    }
14
15    /// Returns whether to round the horizontal offset of a component to the
16    /// pixel grid.
17    ///
18    /// This applies to composite components with the `ROUND_XY_TO_GRID`
19    /// flag. If true, the horizontal offset is rounded along with the
20    /// vertical one. Otherwise, only the vertical offset is rounded. Defaults
21    /// to false.
22    fn round_composite_x_offset(&self) -> bool {
23        false
24    }
25
26    /// Hints one glyph.
27    ///
28    /// A failure stops the load, which reports it as
29    /// [`Error::InvalidOperation`](crate::model::Error::InvalidOperation). A hinter
30    /// that wants the detail keeps the [`HintError`] itself. The hinter
31    /// decides whether a failure is fatal: returning `Ok` carries on with the
32    /// points as the program left them.
33    fn hint(&mut self, zone: &mut GlyphZone<'_, C>) -> Result<(), HintError>;
34}
35
36/// The points a [`Hinter`] works on, zone 1 in the specification's terms.
37///
38/// Unlike [`OutlineRef`](super::OutlineRef) this is mutable, stores its
39/// phantom points alongside the rest, and is not shifted by the left side
40/// bearing.
41pub struct GlyphZone<'a, C: PointCoord> {
42    /// The glyph being hinted.
43    pub glyph: GlyphId,
44    /// The glyph's TrueType instructions.
45    pub bytecode: &'a [u8],
46    /// Scaled points, including phantom points, which hinting moves.
47    pub points: &'a mut [Point<C>],
48    /// The same points in font units.
49    pub unscaled: &'a mut [Point<i32>],
50    /// The flag for each point, which hinting also marks up.
51    pub flags: &'a mut [PointFlags],
52    /// Contour end points, relative to the start of `points`.
53    pub contours: &'a [u16],
54    /// The four phantom points, which hinting may move.
55    pub phantom: &'a mut [Point<C>; PHANTOM_POINT_COUNT],
56    /// True when hinting a composite rather than a simple glyph.
57    pub is_composite: bool,
58    /// Normalized variation coordinates, empty for a static instance.
59    pub coords: &'a [F2Dot14],
60}
61
62#[cfg(test)]
63mod tests {
64    /// Any scaled size will do; the hook does not care which.
65    const PPEM: f32 = 16.0;
66
67    use super::super::{Outline, OutlinePlan, OutlineTables, Scale26Dot6};
68    use super::*;
69    use crate::model::Error;
70    use crate::tables::glyf::bytecode::{HintErrorKind, Opcode, Program};
71    use crate::{
72        types::{F26Dot6, GlyphId},
73        FontRef, TableProvider,
74    };
75
76    /// Records what the hinting hook was handed.
77    #[derive(Default)]
78    struct RecordingHinter {
79        calls: usize,
80        bytecode_len: usize,
81        points: usize,
82        composite: bool,
83    }
84
85    /// An instructed glyph reaches the hinter, with its bytecode and a point
86    /// buffer that includes the phantom points.
87    #[test]
88    fn instructed_glyph_reaches_the_hinter() {
89        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
90        let context = OutlineTables::new(&font).unwrap();
91        let n = font.maxp().unwrap().num_glyphs();
92        let mut outline = Outline::<Scale26Dot6>::new();
93        let mut hinted = 0;
94        for gid in 0..n {
95            let plan = OutlinePlan::new(&context, GlyphId::from(gid)).unwrap();
96            if !plan.has_hinting || plan.glyph_data.is_none() {
97                continue;
98            }
99            let mut hinter = RecordingHinter::default();
100            outline
101                .load_hinted(&context, GlyphId::from(gid), Some(PPEM), &mut hinter)
102                .unwrap();
103            assert_eq!(hinter.calls, 1, "gid {gid}");
104            assert!(hinter.bytecode_len > 0);
105            assert_eq!(hinter.points as u32, plan.num_points);
106            hinted += 1;
107        }
108        assert!(hinted > 0, "expected some instructed glyphs");
109    }
110
111    /// Without a hinter, an instructed glyph is simply loaded unhinted.
112    #[test]
113    fn hinting_is_optional() {
114        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
115        let context = OutlineTables::new(&font).unwrap();
116        let n = font.maxp().unwrap().num_glyphs();
117        let gid = (0..n)
118            .map(GlyphId::from)
119            .find(|gid| {
120                OutlinePlan::new(&context, *gid)
121                    .map(|i| i.has_hinting && i.glyph_data.is_some())
122                    .unwrap_or(false)
123            })
124            .unwrap();
125        let mut outline = Outline::<Scale26Dot6>::new();
126        assert!(outline.load(&context, gid, Some(PPEM)).is_ok());
127    }
128
129    impl Hinter<F26Dot6> for RecordingHinter {
130        fn hint(&mut self, zone: &mut GlyphZone<'_, F26Dot6>) -> Result<(), HintError> {
131            self.calls += 1;
132            self.bytecode_len = zone.bytecode.len();
133            self.points = zone.points.len();
134            self.composite = zone.is_composite;
135            // The font unit copy must line up with the scaled points.
136            assert_eq!(zone.unscaled.len(), zone.points.len());
137            assert!(!zone.contours.is_empty());
138            Ok(())
139        }
140    }
141
142    /// A hinter that always fails, at a place it can name.
143    struct FailingHinter;
144
145    impl Hinter<F26Dot6> for FailingHinter {
146        fn hint(&mut self, zone: &mut GlyphZone<'_, F26Dot6>) -> Result<(), HintError> {
147            Err(HintError {
148                program: Program::Glyph,
149                glyph_id: Some(zone.glyph),
150                pc: 3,
151                opcode: Some(Opcode::SVTCA0),
152                kind: HintErrorKind::ValueStackOverflow,
153            })
154        }
155    }
156
157    /// What the hinter reported reaches the caller, rather than being
158    /// flattened into something the loader made up.
159    #[test]
160    fn a_hinters_error_survives() {
161        let font = FontRef::new(font_test_data::COUSINE_HINT_SUBSET).unwrap();
162        let context = OutlineTables::new(&font).unwrap();
163        let n = font.maxp().unwrap().num_glyphs();
164        let gid = (0..n)
165            .map(GlyphId::from)
166            .find(|gid| {
167                OutlinePlan::new(&context, *gid)
168                    .map(|i| i.has_hinting && i.glyph_data.is_some())
169                    .unwrap_or(false)
170            })
171            .unwrap();
172        let mut outline = Outline::<Scale26Dot6>::new();
173        let err = outline
174            .load_hinted(&context, gid, Some(PPEM), &mut FailingHinter)
175            .unwrap_err();
176        // The hinter keeps its own detail; the load reports the kind.
177        assert_eq!(err, Error::InvalidOperation);
178    }
179}