skrifa/outline/glyf/hint/engine/delta.rs
1//! Managing delta exceptions.
2//!
3//! Implements 6 instructions.
4//!
5//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-exceptions>
6
7use super::{super::graphics::CoordAxis, Engine, F26Dot6, OpResult};
8use read_fonts::tables::glyf::bytecode::Opcode;
9
10impl Engine<'_> {
11 /// Delta exception P1, P2 and P3.
12 ///
13 /// DELTAP1[] (0x5D)
14 /// DELTAP2[] (0x71)
15 /// DELTAP3[] (0x72)
16 ///
17 /// Pops: n: number of pairs of exception specifications and points (uint32)
18 /// p1, arg1, p2, arg2, ..., pnn argn: n pairs of exception specifications
19 /// and points (pairs of uint32s)
20 ///
21 /// DELTAP moves the specified points at the size and by the
22 /// amount specified in the paired argument. An arbitrary number of points
23 /// and arguments can be specified.
24 ///
25 /// The only difference between the instructions is the bias added to the
26 /// point adjustment.
27 ///
28 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#delta-exception-p1>
29 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6509>
30 pub(super) fn op_deltap(&mut self, opcode: Opcode) -> OpResult {
31 let gs = &mut self.graphics;
32 let ppem = gs.ppem as u32;
33 let point_count = gs.zp0().points.len();
34 let n = self.value_stack.pop_count_checked()?;
35 // Each exception requires two values on the stack so limit our
36 // count to prevent looping in non-pedantic mode (where the stack ops
37 // will produce 0 instead of an underflow error)
38 let n = n.min(self.value_stack.len() / 2);
39 let bias = match opcode {
40 Opcode::DELTAP2 => 16,
41 Opcode::DELTAP3 => 32,
42 _ => 0,
43 } + gs.delta_base as u32;
44 let back_compat = gs.backward_compatibility;
45 let did_iup = gs.did_iup_x && gs.did_iup_y;
46 for _ in 0..n {
47 let point_ix = self.value_stack.pop_usize()?;
48 let mut b = self.value_stack.pop()?;
49 // FreeType notes that some popular fonts contain invalid DELTAP
50 // instructions so out of bounds points are ignored.
51 // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6537>
52 if point_ix >= point_count {
53 continue;
54 }
55 let mut c = (b as u32 & 0xF0) >> 4;
56 c += bias;
57 if ppem == c {
58 // Blindly copying FreeType here
59 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6565>
60 b = (b & 0xF) - 8;
61 if b >= 0 {
62 b += 1;
63 }
64 b *= 1 << (6 - gs.delta_shift as i32);
65 let distance = F26Dot6::from_bits(b);
66 if back_compat {
67 if !did_iup
68 && ((gs.is_composite && gs.freedom_vector.y != 0)
69 || gs.zp0().is_touched(point_ix, CoordAxis::Y)?)
70 {
71 gs.move_point(gs.zp0, point_ix, distance)?;
72 }
73 } else {
74 gs.move_point(gs.zp0, point_ix, distance)?;
75 }
76 }
77 }
78 Ok(())
79 }
80
81 /// Delta exception C1, C2 and C3.
82 ///
83 /// DELTAC1[] (0x73)
84 /// DELTAC2[] (0x74)
85 /// DELTAC3[] (0x75)
86 ///
87 /// Pops: n: number of pairs of exception specifications and CVT entry numbers (uint32)
88 /// c1, arg1, c2, arg2,..., cn, argn: (pairs of uint32s)
89 ///
90 /// DELTAC changes the value in each CVT entry specified at the size and
91 /// by the amount specified in its paired argument.
92 ///
93 /// The only difference between the instructions is the bias added to the
94 /// adjustment.
95 ///
96 /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#delta-exception-c1>
97 /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6604>
98 pub(super) fn op_deltac(&mut self, opcode: Opcode) -> OpResult {
99 let gs = &mut self.graphics;
100 let ppem = gs.ppem as u32;
101 let n = self.value_stack.pop_count_checked()?;
102 // Each exception requires two values on the stack so limit our
103 // count to prevent looping in non-pedantic mode (where the stack ops
104 // will produce 0 instead of an underflow error)
105 let n = n.min(self.value_stack.len() / 2);
106 let bias = match opcode {
107 Opcode::DELTAC2 => 16,
108 Opcode::DELTAC3 => 32,
109 _ => 0,
110 } + gs.delta_base as u32;
111 for _ in 0..n {
112 let cvt_ix = self.value_stack.pop_usize()?;
113 let mut b = self.value_stack.pop()?;
114 let mut c = (b as u32 & 0xF0) >> 4;
115 c += bias;
116 if ppem == c {
117 // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6660>
118 b = (b & 0xF) - 8;
119 if b >= 0 {
120 b += 1;
121 }
122 b *= 1 << (6 - gs.delta_shift as i32);
123 let cvt_val = self.cvt.get(cvt_ix)?;
124 self.cvt.set(cvt_ix, cvt_val + F26Dot6::from_bits(b))?;
125 }
126 }
127 Ok(())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::super::{super::zone::ZonePointer, HintErrorKind, MockEngine};
134 use raw::{
135 tables::glyf::bytecode::Opcode,
136 types::{F26Dot6, Point},
137 };
138
139 #[test]
140 fn deltap() {
141 let mut mock = MockEngine::new();
142 let mut engine = mock.engine();
143 engine.graphics.backward_compatibility = false;
144 engine.graphics.zp0 = ZonePointer::Glyph;
145 let raw_ppem = 16;
146 let raw_adjustment = 7;
147 for (point_ix, (ppem_bias, opcode)) in [
148 (0, Opcode::DELTAP1),
149 (16, Opcode::DELTAP2),
150 (32, Opcode::DELTAP3),
151 ]
152 .iter()
153 .enumerate()
154 {
155 let ppem = raw_ppem + ppem_bias;
156 engine.graphics.ppem = ppem;
157 // packed ppem + adjustment entry
158 let packed_ppem = raw_ppem - engine.graphics.delta_base as i32;
159 engine
160 .value_stack
161 .push((packed_ppem << 4) | raw_adjustment)
162 .unwrap();
163 // point index
164 engine.value_stack.push(point_ix as _).unwrap();
165 // exception count
166 engine.value_stack.push(1).unwrap();
167 engine.op_deltap(*opcode).unwrap();
168 let point = engine.graphics.zones[1].point(point_ix).unwrap();
169 assert_eq!(point.map(F26Dot6::to_bits), Point::new(-8, 0));
170 }
171 }
172
173 #[test]
174 fn deltac() {
175 let mut mock = MockEngine::new();
176 let mut engine = mock.engine();
177 let raw_ppem = 16;
178 let raw_adjustment = 7;
179 for (cvt_ix, (ppem_bias, opcode)) in [
180 (0, Opcode::DELTAC1),
181 (16, Opcode::DELTAC2),
182 (32, Opcode::DELTAC3),
183 ]
184 .iter()
185 .enumerate()
186 {
187 let ppem = raw_ppem + ppem_bias;
188 engine.graphics.ppem = ppem;
189 // packed ppem + adjustment entry
190 let packed_ppem = raw_ppem - engine.graphics.delta_base as i32;
191 engine
192 .value_stack
193 .push((packed_ppem << 4) | raw_adjustment)
194 .unwrap();
195 // cvt index
196 engine.value_stack.push(cvt_ix as _).unwrap();
197 // exception count
198 engine.value_stack.push(1).unwrap();
199 engine.op_deltac(*opcode).unwrap();
200 let value = engine.cvt.get(cvt_ix).unwrap();
201 assert_eq!(value.to_bits(), -8);
202 }
203 }
204
205 /// Fuzzer detected timeout when the count supplied for deltap was
206 /// negative. Converting to unsigned resulted in an absurdly high
207 /// number leading to timeout.
208 /// See <https://issues.oss-fuzz.com/issues/42538387>
209 /// and <https://github.com/googlefonts/fontations/issues/1290>
210 #[test]
211 fn deltap_negative_count() {
212 let mut mock = MockEngine::new();
213 let mut engine = mock.engine();
214 // We don't care about the parameters to the instruction except
215 // for the count which is set to -1
216 let stack = [0, 0, -1];
217 // Non-pedantic mode: we end up with a count of 0 so do nothing
218 for value in &stack {
219 engine.value_stack.push(*value).unwrap();
220 }
221 // This just shouldn't hang the tests
222 engine.op_deltap(Opcode::DELTAP3).unwrap();
223 // Pedantic mode: raise an error
224 engine.value_stack.is_pedantic = true;
225 for value in &stack {
226 engine.value_stack.push(*value).unwrap();
227 }
228 assert!(matches!(
229 engine.op_deltap(Opcode::DELTAP3),
230 Err(HintErrorKind::InvalidStackValue(-1))
231 ));
232 }
233
234 /// Copy of the above test for DELTAC
235 #[test]
236 fn deltac_negative_count() {
237 let mut mock = MockEngine::new();
238 let mut engine = mock.engine();
239 // We don't care about the parameters to the instruction except
240 // for the count which is set to -1
241 let stack = [0, 0, -1];
242 // Non-pedantic mode: we end up with a count of 0 so do nothing
243 for value in &stack {
244 engine.value_stack.push(*value).unwrap();
245 }
246 // This just shouldn't hang the tests
247 engine.op_deltac(Opcode::DELTAC3).unwrap();
248 // Pedantic mode: raise an error
249 engine.value_stack.is_pedantic = true;
250 for value in &stack {
251 engine.value_stack.push(*value).unwrap();
252 }
253 assert!(matches!(
254 engine.op_deltac(Opcode::DELTAC3),
255 Err(HintErrorKind::InvalidStackValue(-1))
256 ));
257 }
258}