Skip to main content

skrifa/outline/glyf/hint/engine/
outline.rs

1//! Managing outlines.
2//!
3//! Implements 87 instructions.
4//!
5//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-outlines>
6
7use super::{
8    super::{
9        graphics::CoordAxis,
10        zone::{PointDisplacement, ZonePointer},
11    },
12    math, Engine, F26Dot6, HintErrorKind, OpResult,
13};
14
15impl Engine<'_> {
16    /// Flip point.
17    ///
18    /// FLIPPT[] (0x80)
19    ///
20    /// Pops: p: point number (uint32)
21    ///
22    /// Uses the loop counter.
23    ///
24    /// Flips points that are off the curve so that they are on the curve and
25    /// points that are on the curve so that they are off the curve. The point
26    /// is not marked as touched. The result of a FLIPPT instruction is that
27    /// the contour describing part of a glyph outline is redefined.
28    ///
29    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#flip-point>
30    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5002>
31    pub(super) fn op_flippt(&mut self) -> OpResult {
32        let count = self.graphics.loop_counter as usize;
33        self.graphics.loop_counter = 1;
34        // In backward compatibility mode, don't flip points after IUP has
35        // been done.
36        if self.graphics.backward_compatibility
37            && self.graphics.did_iup_x
38            && self.graphics.did_iup_y
39        {
40            for _ in 0..count {
41                self.value_stack.pop()?;
42            }
43            return Ok(());
44        }
45        let zone = self.graphics.zone_mut(ZonePointer::Glyph);
46        for _ in 0..count {
47            let p = self.value_stack.pop_usize()?;
48            zone.flip_on_curve(p)?;
49        }
50        Ok(())
51    }
52
53    /// Flip range on.
54    ///
55    /// FLIPRGON[] (0x81)
56    ///
57    /// Pops: highpoint: highest point number in range of points to be flipped (uint32)
58    ///       lowpoint: lowest point number in range of points to be flipped (uint32)
59    ///
60    /// Flips a range of points beginning with lowpoint and ending with highpoint so that
61    /// any off the curve points become on the curve points. The points are not marked as
62    /// touched.
63    ///
64    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#flip-range-on>
65    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5056>
66    pub(super) fn op_fliprgon(&mut self) -> OpResult {
67        self.set_on_curve_for_range(true)
68    }
69
70    /// Flip range off.
71    ///
72    /// FLIPRGOFF[] (0x82)
73    ///
74    /// Pops: highpoint: highest point number in range of points to be flipped (uint32)
75    ///       lowpoint: lowest point number in range of points to be flipped (uint32)
76    ///
77    /// Flips a range of points beginning with lowpoint and ending with
78    /// highpoint so that any on the curve points become off the curve points.
79    /// The points are not marked as touched.
80    ///
81    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#flip-range-off>
82    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5094>
83    pub(super) fn op_fliprgoff(&mut self) -> OpResult {
84        self.set_on_curve_for_range(false)
85    }
86
87    /// Shift point by the last point.
88    ///
89    /// SHP\[a\] (0x32 - 0x33)
90    ///
91    /// a: 0: uses rp2 in the zone pointed to by zp1
92    ///    1: uses rp1 in the zone pointed to by zp0
93    ///
94    /// Pops: p: point to be shifted
95    ///
96    /// Uses the loop counter.
97    ///
98    /// Shift point p by the same amount that the reference point has been
99    /// shifted. Point p is shifted along the freedom_vector so that the
100    /// distance between the new position of point p and the current position
101    /// of point p is the same as the distance between the current position
102    /// of the reference point and the original position of the reference point.
103    ///
104    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#shift-point-by-the-last-point>
105    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5211>
106    pub(super) fn op_shp(&mut self, opcode: u8) -> OpResult {
107        let gs = &mut self.graphics;
108        let PointDisplacement { dx, dy, .. } = gs.point_displacement(opcode)?;
109        let count = gs.loop_counter;
110        gs.loop_counter = 1;
111        for _ in 0..count {
112            let p = self.value_stack.pop_usize()?;
113            gs.move_zp2_point(p, dx, dy, true)?;
114        }
115        Ok(())
116    }
117
118    /// Shift contour by the last point.
119    ///
120    /// SHC\[a\] (0x34 - 0x35)
121    ///
122    /// a: 0: uses rp2 in the zone pointed to by zp1
123    ///    1: uses rp1 in the zone pointed to by zp0
124    ///
125    /// Pops: c: contour to be shifted
126    ///
127    /// Shifts every point on contour c by the same amount that the reference
128    /// point has been shifted. Each point is shifted along the freedom_vector
129    /// so that the distance between the new position of the point and the old
130    /// position of that point is the same as the distance between the current
131    /// position of the reference point and the original position of the
132    /// reference point. The distance is measured along the projection_vector.
133    /// If the reference point is one of the points defining the contour, the
134    /// reference point is not moved by this instruction.
135    ///
136    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#shift-contour-by-the-last-point>
137    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5266>
138    pub(super) fn op_shc(&mut self, opcode: u8) -> OpResult {
139        let gs = &mut self.graphics;
140        let contour_ix = self.value_stack.pop_usize()?;
141        if !gs.is_pedantic && contour_ix >= gs.zp2().contours.len() {
142            return Ok(());
143        }
144        let point_disp = gs.point_displacement(opcode)?;
145        let start = if contour_ix != 0 {
146            gs.zp2().contour(contour_ix - 1)? as usize + 1
147        } else {
148            0
149        };
150        let end = if gs.zp2.is_twilight() {
151            gs.zp2().points.len()
152        } else {
153            gs.zp2().contour(contour_ix)? as usize + 1
154        };
155        for i in start..end {
156            if point_disp.zone != gs.zp2 || point_disp.point_ix != i {
157                gs.move_zp2_point(i, point_disp.dx, point_disp.dy, true)?;
158            }
159        }
160        Ok(())
161    }
162
163    /// Shift zone by the last point.
164    ///
165    /// SHZ\[a\] (0x36 - 0x37)
166    ///
167    /// a: 0: uses rp2 in the zone pointed to by zp1
168    ///    1: uses rp1 in the zone pointed to by zp0
169    ///
170    /// Pops: e: zone to be shifted
171    ///
172    /// Shift the points in the specified zone (Z1 or Z0) by the same amount
173    /// that the reference point has been shifted. The points in the zone are
174    /// shifted along the freedom_vector so that the distance between the new
175    /// position of the shifted points and their old position is the same as
176    /// the distance between the current position of the reference point and
177    /// the original position of the reference point.
178    ///
179    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#shift-zone-by-the-last-pt>
180    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5318>
181    pub(super) fn op_shz(&mut self, opcode: u8) -> OpResult {
182        let _e = ZonePointer::try_from(self.value_stack.pop()?)?;
183        let gs = &mut self.graphics;
184        let point_disp = gs.point_displacement(opcode)?;
185        let end = if gs.zp2.is_twilight() {
186            gs.zp2().points.len()
187        } else if !gs.zp2().contours.is_empty() {
188            *gs.zp2()
189                .contours
190                .last()
191                .ok_or(HintErrorKind::InvalidContourIndex(0))? as usize
192                + 1
193        } else {
194            0
195        };
196        for i in 0..end {
197            if point_disp.zone != gs.zp2 || i != point_disp.point_ix {
198                gs.move_zp2_point(i, point_disp.dx, point_disp.dy, false)?;
199            }
200        }
201        Ok(())
202    }
203
204    /// Shift point by a pixel amount.
205    ///
206    /// SHPIX (0x38)
207    ///
208    /// Pops: amount: magnitude of the shift (F26Dot6)
209    ///       p1, p2,.. pn: points to be shifted
210    ///
211    /// Uses the loop counter.
212    ///
213    /// Shifts the points specified by the amount stated. When the loop
214    /// variable is used, the amount to be shifted is put onto the stack
215    /// only once. That is, if loop = 3, then the contents of the top of
216    /// the stack should be point p1, point p2, point p3, amount. The value
217    /// amount is expressed in sixty-fourths of a pixel.
218    ///
219    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#shift-point-by-a-pixel-amount>
220    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5366>
221    pub(super) fn op_shpix(&mut self) -> OpResult {
222        let gs = &mut self.graphics;
223        let in_twilight = gs.zp0.is_twilight() || gs.zp1.is_twilight() || gs.zp2.is_twilight();
224        let amount = self.value_stack.pop()?;
225        let dx = F26Dot6::from_bits(math::mul14(amount, gs.freedom_vector.x));
226        let dy = F26Dot6::from_bits(math::mul14(amount, gs.freedom_vector.y));
227        let count = gs.loop_counter;
228        gs.loop_counter = 1;
229        let did_iup = gs.did_iup_x && gs.did_iup_y;
230        for _ in 0..count {
231            let p = self.value_stack.pop_usize()?;
232            if gs.backward_compatibility {
233                if in_twilight
234                    || (!did_iup
235                        && ((gs.is_composite && gs.freedom_vector.y != 0)
236                            || gs.zp2().is_touched(p, CoordAxis::Y)?))
237                {
238                    gs.move_zp2_point(p, dx, dy, true)?;
239                }
240            } else {
241                gs.move_zp2_point(p, dx, dy, true)?;
242            }
243        }
244        Ok(())
245    }
246
247    /// Move stack indirect relative point.
248    ///
249    /// MSIRP\[a\] (0x3A - 0x3B)
250    ///
251    /// a: 0: do not set rp0 to p
252    ///    1: set rp0 to p
253    ///
254    /// Pops: d: distance (F26Dot6)
255    ///       p: point number
256    ///
257    /// Makes the distance between a point p and rp0 equal to the value
258    /// specified on the stack. The distance on the stack is in fractional
259    /// pixels (F26Dot6). An MSIRP has the same effect as a MIRP instruction
260    /// except that it takes its value from the stack rather than the Control
261    /// Value Table. As a result, the cut_in does not affect the results of a
262    /// MSIRP. Additionally, MSIRP is unaffected by the round_state.
263    ///
264    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-stack-indirect-relative-point>
265    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5439>
266    pub(super) fn op_msirp(&mut self, opcode: u8) -> OpResult {
267        let gs = &mut self.graphics;
268        let distance = self.value_stack.pop_f26dot6()?;
269        let point_ix = self.value_stack.pop_usize()?;
270        if !gs.is_pedantic && !gs.in_bounds([(gs.zp1, point_ix), (gs.zp0, gs.rp0)]) {
271            return Ok(());
272        }
273        if gs.zp1.is_twilight() {
274            *gs.zp1_mut().point_mut(point_ix)? = gs.zp0().original(gs.rp0)?;
275            gs.move_original(gs.zp1, point_ix, distance)?;
276            *gs.zp1_mut().point_mut(point_ix)? = gs.zp1().original(point_ix)?;
277        }
278        let d = gs.project(gs.zp1().point(point_ix)?, gs.zp0().point(gs.rp0)?);
279        gs.move_point(gs.zp1, point_ix, distance.wrapping_sub(d))?;
280        gs.rp1 = gs.rp0;
281        gs.rp2 = point_ix;
282        if (opcode & 1) != 0 {
283            gs.rp0 = point_ix;
284        }
285        Ok(())
286    }
287
288    /// Move direct absolute point.
289    ///
290    /// MDAP\[a\] (0x2E - 0x2F)
291    ///
292    /// a: 0: do not round the value
293    ///    1: round the value
294    ///
295    /// Pops: p: point number
296    ///
297    /// Sets the reference points rp0 and rp1 equal to point p. If a=1, this
298    /// instruction rounds point p to the grid point specified by the state
299    /// variable round_state. If a=0, it simply marks the point as touched in
300    /// the direction(s) specified by the current freedom_vector. This command
301    /// is often used to set points in the twilight zone.
302    ///
303    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-direct-absolute-point>
304    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5487>
305    pub(super) fn op_mdap(&mut self, opcode: u8) -> OpResult {
306        let gs = &mut self.graphics;
307        let p = self.value_stack.pop_usize()?;
308        if !gs.is_pedantic && !gs.in_bounds([(gs.zp0, p)]) {
309            gs.rp0 = p;
310            gs.rp1 = p;
311            return Ok(());
312        }
313        let distance = if (opcode & 1) != 0 {
314            let cur_dist = gs.project(gs.zp0().point(p)?, Default::default());
315            gs.round(cur_dist) - cur_dist
316        } else {
317            F26Dot6::ZERO
318        };
319        gs.move_point(gs.zp0, p, distance)?;
320        gs.rp0 = p;
321        gs.rp1 = p;
322        Ok(())
323    }
324
325    /// Move indirect absolute point.
326    ///
327    /// MIAP\[a\] (0x3E - 0x3F)
328    ///
329    /// a: 0: do not round the distance and don't use control value cutin
330    ///    1: round the distance and use control value cutin
331    ///
332    /// Pops: n: CVT entry number
333    ///       p: point number
334    ///
335    /// Moves point p to the absolute coordinate position specified by the nth
336    /// Control Value Table entry. The coordinate is measured along the current
337    /// projection_vector. If a=1, the position will be rounded as specified by
338    /// round_state. If a=1, and if the device space difference between the CVT
339    /// value and the original position is greater than the
340    /// control_value_cut_in, then the original position will be rounded
341    /// (instead of the CVT value.)
342    ///
343    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-indirect-absolute-point>
344    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5526>
345    pub(super) fn op_miap(&mut self, opcode: u8) -> OpResult {
346        let gs = &mut self.graphics;
347        let cvt_entry = self.value_stack.pop_usize()?;
348        let point_ix = self.value_stack.pop_usize()?;
349        let mut distance = self.cvt.get(cvt_entry)?;
350        if gs.zp0.is_twilight() {
351            // Special behavior for twilight zone.
352            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5548>
353            let fv = gs.freedom_vector;
354            let z = gs.zp0_mut();
355            let original_point = z.original_mut(point_ix)?;
356            original_point.x = F26Dot6::from_bits(math::mul14(distance.to_bits(), fv.x));
357            original_point.y = F26Dot6::from_bits(math::mul14(distance.to_bits(), fv.y));
358            *z.point_mut(point_ix)? = *original_point;
359        }
360        let original_distance = gs.project(gs.zp0().point(point_ix)?, Default::default());
361        if (opcode & 1) != 0 {
362            let delta = (distance.wrapping_sub(original_distance)).abs();
363            if delta > gs.control_value_cutin {
364                distance = original_distance;
365            }
366            distance = gs.round(distance);
367        }
368        gs.move_point(gs.zp0, point_ix, distance.wrapping_sub(original_distance))?;
369        gs.rp0 = point_ix;
370        gs.rp1 = point_ix;
371        Ok(())
372    }
373
374    /// Move direct relative point.
375    ///
376    /// MDRP\[abcde\] (0xC0 - 0xDF)
377    ///
378    /// a: 0: do not set rp0 to point p after move
379    ///    1: do set rp0 to point p after move
380    /// b: 0: do not keep distance greater than or equal to minimum_distance
381    ///    1: keep distance greater than or equal to minimum_distance
382    /// c: 0: do not round distance
383    ///    1: round the distance
384    /// de: distance type for engine characteristic compensation
385    ///
386    /// Pops: p: point number
387    ///       
388    /// MDRP moves point p along the freedom_vector so that the distance from
389    /// its new position to the current position of rp0 is the same as the
390    /// distance between the two points in the original uninstructed outline,
391    /// and then adjusts it to be consistent with the Boolean settings. Note
392    /// that it is only the original positions of rp0 and point p and the
393    /// current position of rp0 that determine the new position of point p
394    /// along the freedom_vector.
395    ///
396    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-direct-relative-point>
397    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5610>
398    pub(super) fn op_mdrp(&mut self, opcode: u8) -> OpResult {
399        let gs = &mut self.graphics;
400        let p = self.value_stack.pop_usize()?;
401        if !gs.is_pedantic && !gs.in_bounds([(gs.zp1, p), (gs.zp0, gs.rp0)]) {
402            gs.rp1 = gs.rp0;
403            gs.rp2 = p;
404            if (opcode & 16) != 0 {
405                gs.rp0 = p;
406            }
407            return Ok(());
408        }
409        let mut original_distance = if gs.zp0.is_twilight() || gs.zp1.is_twilight() {
410            gs.dual_project(gs.zp1().original(p)?, gs.zp0().original(gs.rp0)?)
411        } else {
412            let v1 = gs.zp1().unscaled(p);
413            let v2 = gs.zp0().unscaled(gs.rp0);
414            let dist = gs.dual_project_unscaled(v1, v2);
415            F26Dot6::from_bits(math::mul(dist, gs.unscaled_to_pixels()))
416        };
417        let cutin = gs.single_width_cutin;
418        let value = gs.single_width;
419        if cutin > F26Dot6::ZERO
420            && original_distance < value + cutin
421            && original_distance > value - cutin
422        {
423            original_distance = if original_distance >= F26Dot6::ZERO {
424                value
425            } else {
426                -value
427            };
428        }
429        // round flag
430        let mut distance = if (opcode & 4) != 0 {
431            gs.round(original_distance)
432        } else {
433            original_distance
434        };
435        // minimum distance flag
436        if (opcode & 8) != 0 {
437            let min_distance = gs.min_distance;
438            if original_distance >= F26Dot6::ZERO {
439                if distance < min_distance {
440                    distance = min_distance;
441                }
442            } else if distance > -min_distance {
443                distance = -min_distance;
444            }
445        }
446        original_distance = gs.project(gs.zp1().point(p)?, gs.zp0().point(gs.rp0)?);
447        gs.move_point(gs.zp1, p, distance.wrapping_sub(original_distance))?;
448        gs.rp1 = gs.rp0;
449        gs.rp2 = p;
450        if (opcode & 16) != 0 {
451            gs.rp0 = p;
452        }
453        Ok(())
454    }
455
456    /// Move indirect relative point.
457    ///
458    /// MIRP\[abcde\] (0xE0 - 0xFF)
459    ///
460    /// a: 0: do not set rp0 to point p after move
461    ///    1: do set rp0 to point p after move
462    /// b: 0: do not keep distance greater than or equal to minimum_distance
463    ///    1: keep distance greater than or equal to minimum_distance
464    /// c: 0: do not round distance and do not look at control_value_cutin
465    ///    1: round the distance and look at control_value_cutin
466    /// de: distance type for engine characteristic compensation
467    ///
468    /// Pops: n: CVT entry number
469    ///       p: point number
470    ///       
471    /// A MIRP instruction makes it possible to preserve the distance between
472    /// two points subject to a number of qualifications. Depending upon the
473    /// setting of Boolean flag b, the distance can be kept greater than or
474    /// equal to the value established by the minimum_distance state variable.
475    /// Similarly, the instruction can be set to round the distance according
476    /// to the round_state graphics state variable. The value of the minimum
477    /// distance variable is the smallest possible value the distance between
478    /// two points can be rounded to. Additionally, if the c Boolean is set,
479    /// the MIRP instruction acts subject to the control_value_cut_in. If the
480    /// difference between the actual measurement and the value in the CVT is
481    /// sufficiently small (less than the cut_in_value), the CVT value will be
482    /// used and not the actual value. If the device space difference between
483    /// this distance from the CVT and the single_width_value is smaller than
484    /// the single_width_cut_in, then use the single_width_value rather than
485    /// the outline or Control Value Table distance.
486    ///
487    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-indirect-relative-point>
488    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5731>
489    pub(super) fn op_mirp(&mut self, opcode: u8) -> OpResult {
490        let gs = &mut self.graphics;
491        let n = (self.value_stack.pop()?.wrapping_add(1)) as usize;
492        let p = self.value_stack.pop_usize()?;
493        if !gs.is_pedantic
494            && (!gs.in_bounds([(gs.zp1, p), (gs.zp0, gs.rp0)]) || (n > self.cvt.len()))
495        {
496            gs.rp1 = gs.rp0;
497            if (opcode & 16) != 0 {
498                gs.rp0 = p;
499            }
500            gs.rp2 = p;
501            return Ok(());
502        }
503        let mut cvt_distance = if n == 0 {
504            F26Dot6::ZERO
505        } else {
506            self.cvt.get(n - 1)?
507        };
508        // single width test
509        let cutin = gs.single_width_cutin;
510        let value = gs.single_width;
511        let mut delta = cvt_distance.wrapping_sub(value).abs();
512        if delta < cutin {
513            cvt_distance = if cvt_distance >= F26Dot6::ZERO {
514                value
515            } else {
516                -value
517            };
518        }
519        if gs.zp1.is_twilight() {
520            let fv = gs.freedom_vector;
521            let point = {
522                let d = cvt_distance.to_bits();
523                let p2 = gs.zp0().original(gs.rp0)?;
524                let p1 = gs.zp1_mut().original_mut(p)?;
525                p1.x = p2.x + F26Dot6::from_bits(math::mul(d, fv.x));
526                p1.y = p2.y + F26Dot6::from_bits(math::mul(d, fv.y));
527                *p1
528            };
529            *gs.zp1_mut().point_mut(p)? = point;
530        }
531        let original_distance = gs.dual_project(gs.zp1().original(p)?, gs.zp0().original(gs.rp0)?);
532        let current_distance = gs.project(gs.zp1().point(p)?, gs.zp0().point(gs.rp0)?);
533        // auto flip test
534        if gs.auto_flip && (original_distance.to_bits() ^ cvt_distance.to_bits()) < 0 {
535            cvt_distance = -cvt_distance;
536        }
537        // control value cutin and round
538        let mut distance = if (opcode & 4) != 0 {
539            if gs.zp0 == gs.zp1 {
540                delta = cvt_distance.wrapping_sub(original_distance).abs();
541                if delta > gs.control_value_cutin {
542                    cvt_distance = original_distance;
543                }
544            }
545            gs.round(cvt_distance)
546        } else {
547            cvt_distance
548        };
549        // minimum distance test
550        if (opcode & 8) != 0 {
551            let min_distance = gs.min_distance;
552            if original_distance >= F26Dot6::ZERO {
553                if distance < min_distance {
554                    distance = min_distance
555                };
556            } else if distance > -min_distance {
557                distance = -min_distance
558            }
559        }
560        gs.move_point(gs.zp1, p, distance.wrapping_sub(current_distance))?;
561        gs.rp1 = gs.rp0;
562        if (opcode & 16) != 0 {
563            gs.rp0 = p;
564        }
565        gs.rp2 = p;
566        Ok(())
567    }
568
569    /// Align relative point.
570    ///
571    /// ALIGNRP[] (0x3C)
572    ///
573    /// Pops: p: point number (uint32)
574    ///
575    /// Uses the loop counter.
576    ///
577    /// Reduces the distance between rp0 and point p to zero. Since distance
578    /// is measured along the projection_vector and movement is along the
579    /// freedom_vector, the effect of the instruction is to align points.
580    ///
581    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#align-relative-point>
582    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5882>
583    pub(super) fn op_alignrp(&mut self) -> OpResult {
584        let gs = &mut self.graphics;
585        let count = gs.loop_counter;
586        gs.loop_counter = 1;
587        for _ in 0..count {
588            let p = self.value_stack.pop_usize()?;
589            let distance = gs.project(gs.zp1().point(p)?, gs.zp0().point(gs.rp0)?);
590            gs.move_point(gs.zp1, p, -distance)?;
591        }
592        Ok(())
593    }
594
595    /// Move point to intersection of two lines.
596    ///
597    /// ISECT[] (0x0F)
598    ///
599    /// Pops: b1: end point of line 2
600    ///       b0: start point of line 2
601    ///       a1: end point of line 1
602    ///       a0: start point of line 1
603    ///       p: point to move.
604    ///
605    /// Puts point p at the intersection of the lines A and B. The points a0
606    /// and a1 define line A. Similarly, b0 and b1 define line B. ISECT
607    /// ignores the freedom_vector in moving point p.
608    ///
609    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#moves-point-p-to-the-intersection-of-two-lines>
610    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5934>
611    pub(super) fn op_isect(&mut self) -> OpResult {
612        let gs = &mut self.graphics;
613        let b1 = self.value_stack.pop_usize()?;
614        let b0 = self.value_stack.pop_usize()?;
615        let a1 = self.value_stack.pop_usize()?;
616        let a0 = self.value_stack.pop_usize()?;
617        let point_ix = self.value_stack.pop_usize()?;
618        // Lots of funky fixed point math so just map these to i32 to avoid
619        // a bunch of wrapping/unwrapping.
620        // To shreds you say!
621        let [pa0, pa1] = {
622            let z = gs.zp1();
623            [z.point(a0)?, z.point(a1)?].map(|p| p.map(F26Dot6::to_bits))
624        };
625        let [pb0, pb1] = {
626            let z = gs.zp0();
627            [z.point(b0)?, z.point(b1)?].map(|p| p.map(F26Dot6::to_bits))
628        };
629        let dbx = pb1.x.wrapping_sub(pb0.x);
630        let dby = pb1.y.wrapping_sub(pb0.y);
631        let dax = pa1.x.wrapping_sub(pa0.x);
632        let day = pa1.y.wrapping_sub(pa0.y);
633        let dx = pb0.x.wrapping_sub(pa0.x);
634        let dy = pb0.y.wrapping_sub(pa0.y);
635        use math::mul_div;
636        let discriminant = mul_div(dax, -dby, 0x40).wrapping_add(mul_div(day, dbx, 0x40));
637        let dotproduct = mul_div(dax, dbx, 0x40).wrapping_add(mul_div(day, dby, 0x40));
638        // Useful context from FreeType:
639        //
640        // "The discriminant above is actually a cross product of vectors
641        // da and db. Together with the dot product, they can be used as
642        // surrogates for sine and cosine of the angle between the vectors.
643        // Indeed,
644        //       dotproduct   = |da||db|cos(angle)
645        //       discriminant = |da||db|sin(angle)
646        // We use these equations to reject grazing intersections by
647        // thresholding abs(tan(angle)) at 1/19, corresponding to 3 degrees."
648        //
649        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5986>
650        if discriminant.wrapping_abs().wrapping_mul(19) > dotproduct.wrapping_abs() {
651            let v = mul_div(dx, -dby, 0x40).wrapping_add(mul_div(dy, dbx, 0x40));
652            let x = mul_div(v, dax, discriminant);
653            let y = mul_div(v, day, discriminant);
654            let point = gs.zp2_mut().point_mut(point_ix)?;
655            point.x = F26Dot6::from_bits(pa0.x.wrapping_add(x));
656            point.y = F26Dot6::from_bits(pa0.y.wrapping_add(y));
657        } else {
658            let point = gs.zp2_mut().point_mut(point_ix)?;
659            point.x = F26Dot6::from_bits(
660                (pa0.x
661                    .wrapping_add(pa1.x)
662                    .wrapping_add(pb0.x)
663                    .wrapping_add(pb1.x))
664                    / 4,
665            );
666            point.y = F26Dot6::from_bits(
667                (pa0.y
668                    .wrapping_add(pa1.y)
669                    .wrapping_add(pb0.y)
670                    .wrapping_add(pb1.y))
671                    / 4,
672            );
673        }
674        gs.zp2_mut().touch(point_ix, CoordAxis::Both)?;
675        Ok(())
676    }
677
678    /// Align points.
679    ///
680    /// ALIGNPTS[] (0x27)
681    ///
682    /// Pops: p1: point number
683    ///       p2: point number
684    ///
685    /// Makes the distance between point 1 and point 2 zero by moving both
686    /// along the freedom_vector to the average of both their projections
687    /// along the projection_vector.
688    ///
689    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#align-points>
690    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6030>
691    pub(super) fn op_alignpts(&mut self) -> OpResult {
692        let p2 = self.value_stack.pop_usize()?;
693        let p1 = self.value_stack.pop_usize()?;
694        let gs = &mut self.graphics;
695        let distance = F26Dot6::from_bits(
696            gs.project(gs.zp0().point(p2)?, gs.zp1().point(p1)?)
697                .to_bits()
698                / 2,
699        );
700        gs.move_point(gs.zp1, p1, distance)?;
701        gs.move_point(gs.zp0, p2, -distance)?;
702        Ok(())
703    }
704
705    /// Interpolate point by last relative stretch.
706    ///
707    /// IP[] (0x39)
708    ///
709    /// Pops: p: point number
710    ///
711    /// Uses the loop counter.
712    ///
713    /// Moves point p so that its relationship to rp1 and rp2 is the same as it
714    /// was in the original uninstructed outline. Measurements are made along
715    /// the projection_vector, and movement to satisfy the interpolation
716    /// relationship is constrained to be along the freedom_vector. This
717    /// instruction is not valid if rp1 and rp2 have the same position on the
718    /// projection_vector.
719    ///
720    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#interpolate-point-by-the-last-relative-stretch>
721    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6065>
722    pub(super) fn op_ip(&mut self) -> OpResult {
723        let gs = &mut self.graphics;
724        let count = gs.loop_counter;
725        gs.loop_counter = 1;
726        if !gs.is_pedantic && !gs.in_bounds([(gs.zp0, gs.rp1), (gs.zp1, gs.rp2)]) {
727            return Ok(());
728        }
729        let in_twilight = gs.zp0.is_twilight() || gs.zp1.is_twilight() || gs.zp2.is_twilight();
730        let orus_base = if in_twilight {
731            gs.zp0().original(gs.rp1)?
732        } else {
733            gs.zp0().unscaled(gs.rp1).map(F26Dot6::from_bits)
734        };
735        let cur_base = gs.zp0().point(gs.rp1)?;
736        let old_range = if in_twilight {
737            gs.dual_project(gs.zp1().original(gs.rp2)?, orus_base)
738        } else {
739            gs.dual_project(gs.zp1().unscaled(gs.rp2).map(F26Dot6::from_bits), orus_base)
740        };
741        let cur_range = gs.project(gs.zp1().point(gs.rp2)?, cur_base);
742        for _ in 0..count {
743            let point = self.value_stack.pop_usize()?;
744            if !gs.is_pedantic && !gs.in_bounds([(gs.zp2, point)]) {
745                continue;
746            }
747            let original_distance = if in_twilight {
748                gs.dual_project(gs.zp2().original(point)?, orus_base)
749            } else {
750                gs.dual_project(gs.zp2().unscaled(point).map(F26Dot6::from_bits), orus_base)
751            };
752            let cur_distance = gs.project(gs.zp2().point(point)?, cur_base);
753            let new_distance = if original_distance != F26Dot6::ZERO {
754                if old_range != F26Dot6::ZERO {
755                    F26Dot6::from_bits(math::mul_div(
756                        original_distance.to_bits(),
757                        cur_range.to_bits(),
758                        old_range.to_bits(),
759                    ))
760                } else {
761                    original_distance
762                }
763            } else {
764                F26Dot6::ZERO
765            };
766            gs.move_point(gs.zp2, point, new_distance.wrapping_sub(cur_distance))?;
767        }
768        Ok(())
769    }
770
771    /// Interpolate untouched points through the outline.
772    ///
773    /// IUP\[a\] (0x30 - 0x31)
774    ///
775    /// a: 0: interpolate in the y-direction
776    ///    1: interpolate in the x-direction
777    ///
778    /// Considers a glyph contour by contour, moving any untouched points in
779    /// each contour that are between a pair of touched points. If the
780    /// coordinates of an untouched point were originally between those of
781    /// the touched pair, it is linearly interpolated between the new
782    /// coordinates, otherwise the untouched point is shifted by the amount
783    /// the nearest touched point is shifted.
784    ///
785    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#interpolate-untouched-points-through-the-outline>
786    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6391>
787    pub(super) fn op_iup(&mut self, opcode: u8) -> OpResult {
788        let gs = &mut self.graphics;
789        let axis = if (opcode & 1) != 0 {
790            CoordAxis::X
791        } else {
792            CoordAxis::Y
793        };
794        let mut run = true;
795        // In backward compatibility mode, allow IUP until it has been done on
796        // both axes.
797        if gs.backward_compatibility {
798            if gs.did_iup_x && gs.did_iup_y {
799                run = false;
800            }
801            if axis == CoordAxis::X {
802                gs.did_iup_x = true;
803            } else {
804                gs.did_iup_y = true;
805            }
806        }
807        if run {
808            gs.zone_mut(ZonePointer::Glyph).iup(axis)?;
809        }
810        Ok(())
811    }
812
813    /// Untouch point.
814    ///
815    /// UTP[] (0x29)
816    ///
817    /// Pops: p: point number (uint32)
818    ///
819    /// Marks point p as untouched. A point may be touched in the x direction,
820    /// the y direction, both, or neither. This instruction uses the current
821    /// freedom_vector to determine whether to untouch the point in the
822    /// x-direction, the y direction, or both. Points that are marked as
823    /// untouched will be moved by an IUP (interpolate untouched points)
824    /// instruction. Using UTP you can ensure that a point will be affected
825    /// by IUP even if it was previously touched.
826    ///
827    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#untouch-point>
828    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6222>
829    pub(super) fn op_utp(&mut self) -> OpResult {
830        let p = self.value_stack.pop_usize()?;
831        let coord_axis = match (
832            self.graphics.freedom_vector.x != 0,
833            self.graphics.freedom_vector.y != 0,
834        ) {
835            (true, true) => Some(CoordAxis::Both),
836            (true, false) => Some(CoordAxis::X),
837            (false, true) => Some(CoordAxis::Y),
838            (false, false) => None,
839        };
840        if let Some(coord_axis) = coord_axis {
841            self.graphics.zp0_mut().untouch(p, coord_axis)?;
842        }
843        Ok(())
844    }
845
846    /// Helper for FLIPRGON and FLIPRGOFF.
847    fn set_on_curve_for_range(&mut self, on: bool) -> OpResult {
848        let high_point = self.value_stack.pop_usize()?;
849        let low_point = self.value_stack.pop_usize()?;
850        // high_point is inclusive but Zone::set_on_curve takes an exclusive
851        // range
852        let high_point = high_point
853            .checked_add(1)
854            .ok_or(HintErrorKind::InvalidPointIndex(high_point))?;
855        // In backward compatibility mode, don't flip points after IUP has
856        // been done.
857        if self.graphics.backward_compatibility
858            && self.graphics.did_iup_x
859            && self.graphics.did_iup_y
860        {
861            return Ok(());
862        }
863        self.graphics
864            .zone_mut(ZonePointer::Glyph)
865            .set_on_curve(low_point, high_point, on)
866    }
867}
868
869#[cfg(test)]
870mod tests {
871    use super::{super::MockEngine, math, CoordAxis, Engine, ZonePointer};
872    use raw::{
873        tables::glyf::{bytecode::Opcode, PointMarker},
874        types::{F26Dot6, Point},
875    };
876
877    #[test]
878    fn flip_point() {
879        let mut mock = MockEngine::new();
880        let mut engine = mock.engine();
881        // Points all start as off-curve in the mock engine.
882        // Flip every odd point in the first 10
883        let count = 5;
884        // First, set the loop counter:
885        engine.value_stack.push(count).unwrap();
886        engine.op_sloop().unwrap();
887        // Now push the point indices
888        for i in (1..=9).step_by(2) {
889            engine.value_stack.push(i).unwrap();
890        }
891        assert_eq!(engine.value_stack.len(), count as usize);
892        // And flip!
893        engine.op_flippt().unwrap();
894        let flags = &engine.graphics.zones[1].flags;
895        for i in 0..10 {
896            // Odd points are now on-curve
897            assert_eq!(flags[i].is_on_curve(), i & 1 != 0);
898        }
899    }
900
901    /// Backward compat + IUP state prevents flipping.
902    #[test]
903    fn state_prevents_flip_point() {
904        let mut mock = MockEngine::new();
905        let mut engine = mock.engine();
906        // Points all start as off-curve in the mock engine.
907        // Flip every odd point in the first 10
908        let count = 5;
909        // First, set the loop counter:
910        engine.value_stack.push(count).unwrap();
911        engine.op_sloop().unwrap();
912        // Now push the point indices
913        for i in (1..=9).step_by(2) {
914            engine.value_stack.push(i).unwrap();
915        }
916        assert_eq!(engine.value_stack.len(), count as usize);
917        // Prevent flipping
918        engine.graphics.backward_compatibility = true;
919        engine.graphics.did_iup_x = true;
920        engine.graphics.did_iup_y = true;
921        // But try anyway
922        engine.op_flippt().unwrap();
923        let flags = &engine.graphics.zones[1].flags;
924        for i in 0..10 {
925            // All points are still off-curve
926            assert!(!flags[i].is_on_curve());
927        }
928    }
929
930    #[test]
931    fn flip_range_on_off() {
932        let mut mock = MockEngine::new();
933        let mut engine = mock.engine();
934        // Points all start as off-curve in the mock engine.
935        // Flip 10..=20 on
936        engine.value_stack.push(10).unwrap();
937        engine.value_stack.push(20).unwrap();
938        engine.op_fliprgon().unwrap();
939        for (i, flag) in engine.graphics.zones[1].flags.iter().enumerate() {
940            assert_eq!(flag.is_on_curve(), (10..=20).contains(&i));
941        }
942        // Now flip 12..=15 off
943        engine.value_stack.push(12).unwrap();
944        engine.value_stack.push(15).unwrap();
945        engine.op_fliprgoff().unwrap();
946        for (i, flag) in engine.graphics.zones[1].flags.iter().enumerate() {
947            assert_eq!(
948                flag.is_on_curve(),
949                (10..=11).contains(&i) || (16..=20).contains(&i)
950            );
951        }
952    }
953
954    /// Backward compat + IUP state prevents flipping.
955    #[test]
956    fn state_prevents_flip_range_on_off() {
957        let mut mock = MockEngine::new();
958        let mut engine = mock.engine();
959        // Prevent flipping
960        engine.graphics.backward_compatibility = true;
961        engine.graphics.did_iup_x = true;
962        engine.graphics.did_iup_y = true;
963        // Points all start as off-curve in the mock engine.
964        // Try to flip 10..=20 on
965        engine.value_stack.push(10).unwrap();
966        engine.value_stack.push(20).unwrap();
967        engine.op_fliprgon().unwrap();
968        for flag in engine.graphics.zones[1].flags.iter() {
969            assert!(!flag.is_on_curve());
970        }
971        // Reset all points to on
972        for flag in engine.graphics.zones[1].flags.iter_mut() {
973            flag.set_on_curve();
974        }
975        // Now try to flip 12..=15 off
976        engine.value_stack.push(12).unwrap();
977        engine.value_stack.push(15).unwrap();
978        engine.op_fliprgoff().unwrap();
979        for flag in engine.graphics.zones[1].flags.iter() {
980            assert!(flag.is_on_curve());
981        }
982    }
983
984    #[test]
985    fn untouch_point() {
986        let mut mock = MockEngine::new();
987        let mut engine = mock.engine();
988        // Touch all points in both axes to start.
989        let count = engine.graphics.zones[1].points.len();
990        for i in 0..count {
991            engine.graphics.zones[1].touch(i, CoordAxis::Both).unwrap();
992        }
993        let mut untouch = |point_ix: usize, fx, fy, marker| {
994            assert!(engine.graphics.zp0().flags[point_ix].has_marker(marker));
995            // Untouch axis is based on freedom vector:
996            engine.graphics.freedom_vector.x = fx;
997            engine.graphics.freedom_vector.y = fy;
998            engine.value_stack.push(point_ix as i32).unwrap();
999            engine.op_utp().unwrap();
1000            assert!(!engine.graphics.zp0().flags[point_ix].has_marker(marker));
1001        };
1002        // Untouch point 0 in x axis
1003        untouch(0, 1, 0, PointMarker::TOUCHED_X);
1004        // Untouch point 1 in y axis
1005        untouch(1, 0, 1, PointMarker::TOUCHED_Y);
1006        // untouch point 2 in both axes
1007        untouch(2, 1, 1, PointMarker::TOUCHED);
1008    }
1009
1010    #[test]
1011    fn shp() {
1012        let mut mock = MockEngine::new();
1013        let mut engine = mock.engine();
1014        set_test_vectors(&mut engine);
1015        engine.graphics.backward_compatibility = false;
1016        engine.graphics.zp0 = ZonePointer::Glyph;
1017        engine.graphics.zp2 = ZonePointer::Glyph;
1018        engine.graphics.rp2 = 1;
1019        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1020        point.x = F26Dot6::from_bits(132);
1021        point.y = F26Dot6::from_bits(-256);
1022        engine.value_stack.push(1).unwrap();
1023        engine.op_shp(0).unwrap();
1024        let point = engine.graphics.zones[1].point(1).unwrap();
1025        assert_eq!(point.map(F26Dot6::to_bits), Point::new(136, -254));
1026    }
1027
1028    #[test]
1029    fn shc() {
1030        let mut mock = MockEngine::new();
1031        let mut engine = mock.engine();
1032        set_test_vectors(&mut engine);
1033        engine.graphics.backward_compatibility = false;
1034        engine.graphics.zp0 = ZonePointer::Glyph;
1035        engine.graphics.zp2 = ZonePointer::Glyph;
1036        engine.graphics.rp2 = 1;
1037        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1038        point.x = F26Dot6::from_bits(132);
1039        point.y = F26Dot6::from_bits(-256);
1040        engine.value_stack.push(0).unwrap();
1041        engine.op_shc(0).unwrap();
1042        let points = engine.graphics.zones[1]
1043            .points
1044            .iter()
1045            .map(|p| p.map(F26Dot6::to_bits))
1046            .take(3)
1047            .collect::<Vec<_>>();
1048        assert_eq!(
1049            points,
1050            &[Point::new(4, 2), Point::new(132, -256), Point::new(4, 2),]
1051        );
1052    }
1053
1054    #[test]
1055    fn shz() {
1056        let mut mock = MockEngine::new();
1057        let mut engine = mock.engine();
1058        set_test_vectors(&mut engine);
1059        engine.graphics.backward_compatibility = false;
1060        engine.graphics.zp0 = ZonePointer::Glyph;
1061        engine.graphics.zp2 = ZonePointer::Glyph;
1062        engine.graphics.rp2 = 1;
1063        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1064        point.x = F26Dot6::from_bits(132);
1065        point.y = F26Dot6::from_bits(-256);
1066        engine.value_stack.push(0).unwrap();
1067        engine.op_shz(0).unwrap();
1068        let points = engine.graphics.zones[1]
1069            .points
1070            .iter()
1071            .map(|p| p.map(F26Dot6::to_bits))
1072            .take(3)
1073            .collect::<Vec<_>>();
1074        assert_eq!(
1075            points,
1076            &[Point::new(4, 2), Point::new(132, -256), Point::new(4, 2),]
1077        );
1078    }
1079
1080    #[test]
1081    fn shpix() {
1082        let mut mock = MockEngine::new();
1083        let mut engine = mock.engine();
1084        set_test_vectors(&mut engine);
1085        engine.graphics.backward_compatibility = false;
1086        engine.graphics.zp2 = ZonePointer::Glyph;
1087        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1088        point.x = F26Dot6::from_bits(132);
1089        point.y = F26Dot6::from_bits(-256);
1090        // point index
1091        engine.value_stack.push(1).unwrap();
1092        // amount to move in pixels along freedom vector
1093        engine.value_stack.push(42).unwrap();
1094        engine.op_shpix().unwrap();
1095        let point = engine.graphics.zones[1].point(1).unwrap();
1096        assert_eq!(point.map(F26Dot6::to_bits), Point::new(170, -237));
1097    }
1098
1099    #[test]
1100    fn msirp() {
1101        let mut mock = MockEngine::new();
1102        let mut engine = mock.engine();
1103        set_test_vectors(&mut engine);
1104        engine.graphics.backward_compatibility = false;
1105        engine.graphics.zp0 = ZonePointer::Glyph;
1106        engine.graphics.zp1 = ZonePointer::Glyph;
1107        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1108        point.x = F26Dot6::from_bits(132);
1109        point.y = F26Dot6::from_bits(-256);
1110        // point index
1111        engine.value_stack.push(1).unwrap();
1112        // amount to move in pixels along freedom vector
1113        engine.value_stack.push(-42).unwrap();
1114        engine.op_msirp(0).unwrap();
1115        let point = engine.graphics.zones[1].point(1).unwrap();
1116        assert_eq!(point.map(F26Dot6::to_bits), Point::new(91, -277));
1117        assert_eq!(engine.graphics.rp0, 0);
1118        // opcode with bit 0 set changes rp0 to point_ix
1119        engine.value_stack.push(4).unwrap();
1120        engine.value_stack.push(0).unwrap();
1121        engine.op_msirp(1).unwrap();
1122        assert_eq!(engine.graphics.rp0, 4);
1123    }
1124
1125    #[test]
1126    fn mdap() {
1127        let mut mock = MockEngine::new();
1128        let mut engine = mock.engine();
1129        set_test_vectors(&mut engine);
1130        engine.graphics.backward_compatibility = false;
1131        engine.graphics.zp0 = ZonePointer::Glyph;
1132        // with rounding
1133        engine.set_point_f26dot6(1, 1, (132, -256));
1134        engine.value_stack.push(1).unwrap();
1135        engine.op_mdap(1).unwrap();
1136        let point = engine.graphics.zones[1].point(1).unwrap();
1137        assert_eq!(point.map(F26Dot6::to_bits), Point::new(128, -258));
1138        // without rounding
1139        engine.set_point_f26dot6(1, 2, (132, -256));
1140        engine.value_stack.push(2).unwrap();
1141        engine.op_mdap(0).unwrap();
1142        let point = engine.graphics.zones[1].point(2).unwrap();
1143        assert_eq!(point.map(F26Dot6::to_bits), Point::new(132, -256));
1144    }
1145
1146    #[test]
1147    fn miap() {
1148        let mut mock = MockEngine::new();
1149        let mut engine = mock.engine();
1150        set_test_vectors(&mut engine);
1151        engine.graphics.backward_compatibility = false;
1152        engine.graphics.zp0 = ZonePointer::Glyph;
1153        // set a CVT distance
1154        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1155        // with rounding
1156        engine.set_point_f26dot6(1, 1, (132, -256));
1157        engine.value_stack.push(1).unwrap();
1158        engine.value_stack.push(1).unwrap();
1159        engine.op_miap(1).unwrap();
1160        let point = engine.graphics.zones[1].point(1).unwrap();
1161        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1162        // without rounding
1163        engine.set_point_f26dot6(1, 2, (132, -256));
1164        engine.value_stack.push(2).unwrap();
1165        engine.value_stack.push(1).unwrap();
1166        engine.op_miap(0).unwrap();
1167        let point = engine.graphics.zones[1].point(2).unwrap();
1168        assert_eq!(point.map(F26Dot6::to_bits), Point::new(171, -236));
1169    }
1170
1171    /// Tests bit 'a' of MDRP which just sets rp0 to the adjusted point
1172    /// after move.
1173    #[test]
1174    fn mdrp_rp0() {
1175        let mut mock = MockEngine::new();
1176        let mut engine = mock.engine();
1177        engine.graphics.rp0 = 0;
1178        // Don't change rp0
1179        engine.value_stack.push(1).unwrap();
1180        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1181        assert_eq!(engine.graphics.rp0, 0);
1182        // Change rp0
1183        engine.value_stack.push(1).unwrap();
1184        engine.op_mdrp(Opcode::MDRP10000 as _).unwrap();
1185        assert_eq!(engine.graphics.rp0, 1);
1186    }
1187
1188    /// Test bit "b" which controls whether distances are adjusted
1189    /// to the minimum_distance field of GraphicsState.
1190    #[test]
1191    fn mdrp_mindist() {
1192        let mut mock = MockEngine::new();
1193        let mut engine = mock.engine();
1194        set_test_vectors(&mut engine);
1195        engine.graphics.backward_compatibility = false;
1196        engine.graphics.zp0 = ZonePointer::Glyph;
1197        // without min distance check
1198        engine.set_point_f26dot6(1, 1, (132, -256));
1199        engine.value_stack.push(1).unwrap();
1200        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1201        let point = engine.graphics.zones[1].point(1).unwrap();
1202        assert_eq!(point.map(F26Dot6::to_bits), Point::new(128, -258));
1203        // with min distance check
1204        engine.set_point_f26dot6(1, 2, (132, -256));
1205        engine.value_stack.push(2).unwrap();
1206        engine.op_mdrp(Opcode::MDRP01000 as _).unwrap();
1207        let point = engine.graphics.zones[1].point(2).unwrap();
1208        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1209    }
1210
1211    /// Test bit "c" which controls whether distances are rounded.
1212    #[test]
1213    fn mdrp_round() {
1214        let mut mock = MockEngine::new();
1215        let mut engine = mock.engine();
1216        set_test_vectors(&mut engine);
1217        engine.graphics.backward_compatibility = false;
1218        engine.graphics.zp0 = ZonePointer::Glyph;
1219        engine.op_rthg().unwrap();
1220        // without rounding
1221        engine.set_point_f26dot6(1, 1, (132, -231));
1222        engine.value_stack.push(1).unwrap();
1223        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1224        let point = engine.graphics.zones[1].point(1).unwrap();
1225        assert_eq!(point.map(F26Dot6::to_bits), Point::new(119, -238));
1226        // with rounding
1227        engine.set_point_f26dot6(1, 2, (132, -231));
1228        engine.value_stack.push(2).unwrap();
1229        engine.op_mdrp(Opcode::MDRP00100 as _).unwrap();
1230        let point = engine.graphics.zones[1].point(2).unwrap();
1231        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1232    }
1233
1234    /// Tests bit 'a' of MIRP which just sets rp0 to the adjusted point
1235    /// after move.
1236    #[test]
1237    fn mirp_rp0() {
1238        let mut mock = MockEngine::new();
1239        let mut engine = mock.engine();
1240        engine.graphics.rp0 = 0;
1241        // Don't change rp0
1242        engine.value_stack.push(1).unwrap();
1243        engine.value_stack.push(1).unwrap();
1244        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1245        assert_eq!(engine.graphics.rp0, 0);
1246        // Change rp0
1247        engine.value_stack.push(1).unwrap();
1248        engine.value_stack.push(1).unwrap();
1249        engine.op_mirp(Opcode::MIRP10000 as _).unwrap();
1250        assert_eq!(engine.graphics.rp0, 1);
1251    }
1252
1253    /// Test bit "b" which controls whether distances are adjusted
1254    /// to the minimum_distance field of GraphicsState.
1255    #[test]
1256    fn mirp_mindist() {
1257        let mut mock = MockEngine::new();
1258        let mut engine = mock.engine();
1259        set_test_vectors(&mut engine);
1260        engine.graphics.backward_compatibility = false;
1261        engine.graphics.zp0 = ZonePointer::Glyph;
1262        // set a CVT distance
1263        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1264        // without min distance check
1265        engine.set_point_f26dot6(1, 1, (132, -256));
1266        engine.value_stack.push(1).unwrap();
1267        engine.value_stack.push(1).unwrap();
1268        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1269        let point = engine.graphics.zones[1].point(1).unwrap();
1270        assert_eq!(point.map(F26Dot6::to_bits), Point::new(171, -236));
1271        // with min distance check
1272        engine.set_point_f26dot6(1, 2, (132, -256));
1273        engine.value_stack.push(2).unwrap();
1274        engine.value_stack.push(1).unwrap();
1275        engine.op_mirp(Opcode::MIRP01000 as _).unwrap();
1276        let point = engine.graphics.zones[1].point(2).unwrap();
1277        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1278    }
1279
1280    /// Test bit "c" which controls whether distances are rounded.
1281    #[test]
1282    fn mirp_round() {
1283        let mut mock = MockEngine::new();
1284        let mut engine = mock.engine();
1285        set_test_vectors(&mut engine);
1286        engine.graphics.backward_compatibility = false;
1287        engine.graphics.zp0 = ZonePointer::Glyph;
1288        // set a CVT distance
1289        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1290        engine.op_rthg().unwrap();
1291        // without rounding
1292        engine.set_point_f26dot6(1, 1, (132, -231));
1293        engine.value_stack.push(1).unwrap();
1294        engine.value_stack.push(1).unwrap();
1295        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1296        let point = engine.graphics.zones[1].point(1).unwrap();
1297        assert_eq!(point.map(F26Dot6::to_bits), Point::new(162, -216));
1298        // with rounding
1299        engine.set_point_f26dot6(1, 2, (132, -231));
1300        engine.value_stack.push(2).unwrap();
1301        engine.value_stack.push(1).unwrap();
1302        engine.op_mirp(Opcode::MIRP00100 as _).unwrap();
1303        let point = engine.graphics.zones[1].point(2).unwrap();
1304        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1305    }
1306
1307    #[test]
1308    fn mirp_does_not_panic_with_overflow() {
1309        let mut mock = MockEngine::new();
1310        let mut engine = mock.engine();
1311        engine.value_stack.push(i32::MAX).unwrap();
1312        // Just don't panic on overflow
1313        engine.op_mirp(Opcode::MIRP10000 as _).unwrap();
1314    }
1315
1316    #[test]
1317    fn alignrp() {
1318        let mut mock = MockEngine::new();
1319        let mut engine = mock.engine();
1320        set_test_vectors(&mut engine);
1321        engine.graphics.backward_compatibility = false;
1322        engine.graphics.zp0 = ZonePointer::Glyph;
1323        engine.graphics.zp1 = ZonePointer::Glyph;
1324        engine.graphics.rp0 = 0;
1325        engine.set_point_f26dot6(1, 0, (132, -231));
1326        engine.set_point_f26dot6(1, 1, (-72, 109));
1327        engine.value_stack.push(1).unwrap();
1328        engine.op_alignrp().unwrap();
1329        let point = engine.graphics.zones[1].point(1).unwrap();
1330        assert_eq!(point.map(F26Dot6::to_bits), Point::new(-45, 122));
1331    }
1332
1333    #[test]
1334    fn isect() {
1335        let mut mock = MockEngine::new();
1336        let mut engine = mock.engine();
1337        engine.graphics.zp0 = ZonePointer::Glyph;
1338        engine.graphics.zp1 = ZonePointer::Glyph;
1339        engine.graphics.rp0 = 0;
1340        // Two points for line 1
1341        engine.set_point_f26dot6(1, 0, (0, 0));
1342        engine.set_point_f26dot6(1, 1, (100, 100));
1343        // And two more for line 2
1344        engine.set_point_f26dot6(1, 2, (0, 100));
1345        engine.set_point_f26dot6(1, 3, (100, 0));
1346        // Push point numbers: first is the point where the
1347        // intersection should be stored.
1348        for ix in [4, 0, 1, 2, 3] {
1349            engine.value_stack.push(ix).unwrap();
1350        }
1351        engine.op_isect().unwrap();
1352        let point = engine.graphics.zones[1].point(4).unwrap();
1353        assert_eq!(point.map(F26Dot6::to_bits), Point::new(50, 50));
1354    }
1355
1356    #[test]
1357    fn isect_does_not_panic_with_extreme_coords() {
1358        let mut mock = MockEngine::new();
1359        let mut engine = mock.engine();
1360        engine.graphics.zp0 = ZonePointer::Glyph;
1361        engine.graphics.zp1 = ZonePointer::Glyph;
1362        engine.graphics.zp2 = ZonePointer::Glyph;
1363        // Set two lines with extreme coordinates to stress the wrapping math.
1364        engine.set_point_f26dot6(1, 0, (i32::MIN, i32::MAX));
1365        engine.set_point_f26dot6(1, 1, (i32::MAX, i32::MIN));
1366        engine.set_point_f26dot6(1, 2, (i32::MAX, i32::MAX));
1367        engine.set_point_f26dot6(1, 3, (i32::MIN, i32::MIN));
1368        for ix in [4, 0, 1, 2, 3] {
1369            engine.value_stack.push(ix).unwrap();
1370        }
1371        // Don't panic on overflow!
1372        engine.op_isect().unwrap();
1373    }
1374
1375    #[test]
1376    fn alignpts() {
1377        let mut mock = MockEngine::new();
1378        let mut engine = mock.engine();
1379        set_test_vectors(&mut engine);
1380        engine.graphics.backward_compatibility = false;
1381        engine.graphics.zp0 = ZonePointer::Glyph;
1382        engine.graphics.zp1 = ZonePointer::Glyph;
1383        engine.set_point_f26dot6(1, 0, (132, -231));
1384        engine.set_point_f26dot6(1, 1, (-72, 109));
1385        engine.value_stack.push(0).unwrap();
1386        engine.value_stack.push(1).unwrap();
1387        engine.op_alignpts().unwrap();
1388        let p1 = engine.graphics.zones[1].point(0).unwrap();
1389        let p2 = engine.graphics.zones[1].point(1).unwrap();
1390        assert_eq!(p1.map(F26Dot6::to_bits), Point::new(119, -238));
1391        assert_eq!(p2.map(F26Dot6::to_bits), Point::new(-59, 116));
1392    }
1393
1394    #[test]
1395    fn ip() {
1396        let mut mock = MockEngine::new();
1397        let mut engine = mock.engine();
1398        set_test_vectors(&mut engine);
1399        engine.graphics.backward_compatibility = false;
1400        engine.graphics.zp0 = ZonePointer::Glyph;
1401        engine.graphics.zp1 = ZonePointer::Glyph;
1402        engine.graphics.zp2 = ZonePointer::Glyph;
1403        engine.graphics.rp1 = 2;
1404        engine.graphics.rp2 = 3;
1405        engine.set_point_f26dot6(1, 2, (72, -109));
1406        engine.set_point_f26dot6(1, 1, (132, -231));
1407        engine.value_stack.push(1).unwrap();
1408        engine.op_ip().unwrap();
1409        let point = engine.graphics.zones[1].point(1).unwrap();
1410        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1411    }
1412
1413    #[test]
1414    fn iup_flags() {
1415        // IUP shift and interpolate logic is tested in ../zone.rs so just
1416        // check the flags here.
1417        let mut mock = MockEngine::new();
1418        let mut engine = mock.engine();
1419        assert!(!engine.graphics.did_iup_x);
1420        assert!(!engine.graphics.did_iup_y);
1421        // IUP[y]
1422        engine.op_iup(0).unwrap();
1423        assert!(!engine.graphics.did_iup_x);
1424        assert!(engine.graphics.did_iup_y);
1425        // IUP[x]
1426        engine.op_iup(1).unwrap();
1427        assert!(engine.graphics.did_iup_x);
1428        assert!(engine.graphics.did_iup_y);
1429    }
1430
1431    // Add with overflow caught by fuzzer:
1432    // https://issues.oss-fuzz.com/issues/377736138
1433    #[test]
1434    fn flip_region_avoid_overflow() {
1435        let mut mock = MockEngine::new();
1436        let mut engine = mock.engine();
1437        engine.value_stack.push(1).unwrap();
1438        engine.value_stack.push(-1).unwrap();
1439        // Just don't panic
1440        let _ = engine.set_on_curve_for_range(true);
1441    }
1442
1443    fn set_test_vectors(engine: &mut Engine) {
1444        let v = math::normalize14(100, 50);
1445        engine.graphics.proj_vector = v;
1446        engine.graphics.dual_proj_vector = v;
1447        engine.graphics.freedom_vector = v;
1448        engine.graphics.update_projection_state();
1449    }
1450
1451    impl Engine<'_> {
1452        fn set_point_f26dot6(&mut self, zone_ix: usize, point_ix: usize, xy: (i32, i32)) {
1453            let p = self.graphics.zones[zone_ix].point_mut(point_ix).unwrap();
1454            p.x = F26Dot6::from_bits(xy.0);
1455            p.y = F26Dot6::from_bits(xy.1);
1456        }
1457    }
1458}