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}