read_fonts/tables/glyf/outline/
hinter.rs1use crate::tables::glyf::{bytecode::HintError, PointCoord, PointFlags, PHANTOM_POINT_COUNT};
4use crate::types::{F2Dot14, GlyphId, Point};
5
6pub trait Hinter<C: PointCoord> {
9 fn backward_compatibility(&self) -> bool {
12 true
13 }
14
15 fn round_composite_x_offset(&self) -> bool {
23 false
24 }
25
26 fn hint(&mut self, zone: &mut GlyphZone<'_, C>) -> Result<(), HintError>;
34}
35
36pub struct GlyphZone<'a, C: PointCoord> {
42 pub glyph: GlyphId,
44 pub bytecode: &'a [u8],
46 pub points: &'a mut [Point<C>],
48 pub unscaled: &'a mut [Point<i32>],
50 pub flags: &'a mut [PointFlags],
52 pub contours: &'a [u16],
54 pub phantom: &'a mut [Point<C>; PHANTOM_POINT_COUNT],
56 pub is_composite: bool,
58 pub coords: &'a [F2Dot14],
60}
61
62#[cfg(test)]
63mod tests {
64 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 #[derive(Default)]
78 struct RecordingHinter {
79 calls: usize,
80 bytecode_len: usize,
81 points: usize,
82 composite: bool,
83 }
84
85 #[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 #[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 assert_eq!(zone.unscaled.len(), zone.points.len());
137 assert!(!zone.contours.is_empty());
138 Ok(())
139 }
140 }
141
142 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 #[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 assert_eq!(err, Error::InvalidOperation);
178 }
179}