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()? + 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 - pb0.x;
630        let dby = pb1.y - pb0.y;
631        let dax = pa1.x - pa0.x;
632        let day = pa1.y - pa0.y;
633        let dx = pb0.x - pa0.x;
634        let dy = pb0.y - pa0.y;
635        use math::mul_div;
636        let discriminant = mul_div(dax, -dby, 0x40) + mul_div(day, dbx, 0x40);
637        let dotproduct = mul_div(dax, dbx, 0x40) + 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.abs() {
651            let v = mul_div(dx, -dby, 0x40) + 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 + x);
656            point.y = F26Dot6::from_bits(pa0.y + y);
657        } else {
658            let point = gs.zp2_mut().point_mut(point_ix)?;
659            point.x = F26Dot6::from_bits((pa0.x + pa1.x + pb0.x + pb1.x) / 4);
660            point.y = F26Dot6::from_bits((pa0.y + pa1.y + pb0.y + pb1.y) / 4);
661        }
662        gs.zp2_mut().touch(point_ix, CoordAxis::Both)?;
663        Ok(())
664    }
665
666    /// Align points.
667    ///
668    /// ALIGNPTS[] (0x27)
669    ///
670    /// Pops: p1: point number
671    ///       p2: point number
672    ///
673    /// Makes the distance between point 1 and point 2 zero by moving both
674    /// along the freedom_vector to the average of both their projections
675    /// along the projection_vector.
676    ///
677    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#align-points>
678    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6030>
679    pub(super) fn op_alignpts(&mut self) -> OpResult {
680        let p2 = self.value_stack.pop_usize()?;
681        let p1 = self.value_stack.pop_usize()?;
682        let gs = &mut self.graphics;
683        let distance = F26Dot6::from_bits(
684            gs.project(gs.zp0().point(p2)?, gs.zp1().point(p1)?)
685                .to_bits()
686                / 2,
687        );
688        gs.move_point(gs.zp1, p1, distance)?;
689        gs.move_point(gs.zp0, p2, -distance)?;
690        Ok(())
691    }
692
693    /// Interpolate point by last relative stretch.
694    ///
695    /// IP[] (0x39)
696    ///
697    /// Pops: p: point number
698    ///
699    /// Uses the loop counter.
700    ///
701    /// Moves point p so that its relationship to rp1 and rp2 is the same as it
702    /// was in the original uninstructed outline. Measurements are made along
703    /// the projection_vector, and movement to satisfy the interpolation
704    /// relationship is constrained to be along the freedom_vector. This
705    /// instruction is not valid if rp1 and rp2 have the same position on the
706    /// projection_vector.
707    ///
708    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#interpolate-point-by-the-last-relative-stretch>
709    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6065>
710    pub(super) fn op_ip(&mut self) -> OpResult {
711        let gs = &mut self.graphics;
712        let count = gs.loop_counter;
713        gs.loop_counter = 1;
714        if !gs.is_pedantic && !gs.in_bounds([(gs.zp0, gs.rp1), (gs.zp1, gs.rp2)]) {
715            return Ok(());
716        }
717        let in_twilight = gs.zp0.is_twilight() || gs.zp1.is_twilight() || gs.zp2.is_twilight();
718        let orus_base = if in_twilight {
719            gs.zp0().original(gs.rp1)?
720        } else {
721            gs.zp0().unscaled(gs.rp1).map(F26Dot6::from_bits)
722        };
723        let cur_base = gs.zp0().point(gs.rp1)?;
724        let old_range = if in_twilight {
725            gs.dual_project(gs.zp1().original(gs.rp2)?, orus_base)
726        } else {
727            gs.dual_project(gs.zp1().unscaled(gs.rp2).map(F26Dot6::from_bits), orus_base)
728        };
729        let cur_range = gs.project(gs.zp1().point(gs.rp2)?, cur_base);
730        for _ in 0..count {
731            let point = self.value_stack.pop_usize()?;
732            if !gs.is_pedantic && !gs.in_bounds([(gs.zp2, point)]) {
733                continue;
734            }
735            let original_distance = if in_twilight {
736                gs.dual_project(gs.zp2().original(point)?, orus_base)
737            } else {
738                gs.dual_project(gs.zp2().unscaled(point).map(F26Dot6::from_bits), orus_base)
739            };
740            let cur_distance = gs.project(gs.zp2().point(point)?, cur_base);
741            let new_distance = if original_distance != F26Dot6::ZERO {
742                if old_range != F26Dot6::ZERO {
743                    F26Dot6::from_bits(math::mul_div(
744                        original_distance.to_bits(),
745                        cur_range.to_bits(),
746                        old_range.to_bits(),
747                    ))
748                } else {
749                    original_distance
750                }
751            } else {
752                F26Dot6::ZERO
753            };
754            gs.move_point(gs.zp2, point, new_distance.wrapping_sub(cur_distance))?;
755        }
756        Ok(())
757    }
758
759    /// Interpolate untouched points through the outline.
760    ///
761    /// IUP\[a\] (0x30 - 0x31)
762    ///
763    /// a: 0: interpolate in the y-direction
764    ///    1: interpolate in the x-direction
765    ///
766    /// Considers a glyph contour by contour, moving any untouched points in
767    /// each contour that are between a pair of touched points. If the
768    /// coordinates of an untouched point were originally between those of
769    /// the touched pair, it is linearly interpolated between the new
770    /// coordinates, otherwise the untouched point is shifted by the amount
771    /// the nearest touched point is shifted.
772    ///
773    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#interpolate-untouched-points-through-the-outline>
774    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6391>
775    pub(super) fn op_iup(&mut self, opcode: u8) -> OpResult {
776        let gs = &mut self.graphics;
777        let axis = if (opcode & 1) != 0 {
778            CoordAxis::X
779        } else {
780            CoordAxis::Y
781        };
782        let mut run = true;
783        // In backward compatibility mode, allow IUP until it has been done on
784        // both axes.
785        if gs.backward_compatibility {
786            if gs.did_iup_x && gs.did_iup_y {
787                run = false;
788            }
789            if axis == CoordAxis::X {
790                gs.did_iup_x = true;
791            } else {
792                gs.did_iup_y = true;
793            }
794        }
795        if run {
796            gs.zone_mut(ZonePointer::Glyph).iup(axis)?;
797        }
798        Ok(())
799    }
800
801    /// Untouch point.
802    ///
803    /// UTP[] (0x29)
804    ///
805    /// Pops: p: point number (uint32)
806    ///
807    /// Marks point p as untouched. A point may be touched in the x direction,
808    /// the y direction, both, or neither. This instruction uses the current
809    /// freedom_vector to determine whether to untouch the point in the
810    /// x-direction, the y direction, or both. Points that are marked as
811    /// untouched will be moved by an IUP (interpolate untouched points)
812    /// instruction. Using UTP you can ensure that a point will be affected
813    /// by IUP even if it was previously touched.
814    ///
815    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#untouch-point>
816    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6222>
817    pub(super) fn op_utp(&mut self) -> OpResult {
818        let p = self.value_stack.pop_usize()?;
819        let coord_axis = match (
820            self.graphics.freedom_vector.x != 0,
821            self.graphics.freedom_vector.y != 0,
822        ) {
823            (true, true) => Some(CoordAxis::Both),
824            (true, false) => Some(CoordAxis::X),
825            (false, true) => Some(CoordAxis::Y),
826            (false, false) => None,
827        };
828        if let Some(coord_axis) = coord_axis {
829            self.graphics.zp0_mut().untouch(p, coord_axis)?;
830        }
831        Ok(())
832    }
833
834    /// Helper for FLIPRGON and FLIPRGOFF.
835    fn set_on_curve_for_range(&mut self, on: bool) -> OpResult {
836        let high_point = self.value_stack.pop_usize()?;
837        let low_point = self.value_stack.pop_usize()?;
838        // high_point is inclusive but Zone::set_on_curve takes an exclusive
839        // range
840        let high_point = high_point
841            .checked_add(1)
842            .ok_or(HintErrorKind::InvalidPointIndex(high_point))?;
843        // In backward compatibility mode, don't flip points after IUP has
844        // been done.
845        if self.graphics.backward_compatibility
846            && self.graphics.did_iup_x
847            && self.graphics.did_iup_y
848        {
849            return Ok(());
850        }
851        self.graphics
852            .zone_mut(ZonePointer::Glyph)
853            .set_on_curve(low_point, high_point, on)
854    }
855}
856
857#[cfg(test)]
858mod tests {
859    use super::{super::MockEngine, math, CoordAxis, Engine, ZonePointer};
860    use raw::{
861        tables::glyf::{bytecode::Opcode, PointMarker},
862        types::{F26Dot6, Point},
863    };
864
865    #[test]
866    fn flip_point() {
867        let mut mock = MockEngine::new();
868        let mut engine = mock.engine();
869        // Points all start as off-curve in the mock engine.
870        // Flip every odd point in the first 10
871        let count = 5;
872        // First, set the loop counter:
873        engine.value_stack.push(count).unwrap();
874        engine.op_sloop().unwrap();
875        // Now push the point indices
876        for i in (1..=9).step_by(2) {
877            engine.value_stack.push(i).unwrap();
878        }
879        assert_eq!(engine.value_stack.len(), count as usize);
880        // And flip!
881        engine.op_flippt().unwrap();
882        let flags = &engine.graphics.zones[1].flags;
883        for i in 0..10 {
884            // Odd points are now on-curve
885            assert_eq!(flags[i].is_on_curve(), i & 1 != 0);
886        }
887    }
888
889    /// Backward compat + IUP state prevents flipping.
890    #[test]
891    fn state_prevents_flip_point() {
892        let mut mock = MockEngine::new();
893        let mut engine = mock.engine();
894        // Points all start as off-curve in the mock engine.
895        // Flip every odd point in the first 10
896        let count = 5;
897        // First, set the loop counter:
898        engine.value_stack.push(count).unwrap();
899        engine.op_sloop().unwrap();
900        // Now push the point indices
901        for i in (1..=9).step_by(2) {
902            engine.value_stack.push(i).unwrap();
903        }
904        assert_eq!(engine.value_stack.len(), count as usize);
905        // Prevent flipping
906        engine.graphics.backward_compatibility = true;
907        engine.graphics.did_iup_x = true;
908        engine.graphics.did_iup_y = true;
909        // But try anyway
910        engine.op_flippt().unwrap();
911        let flags = &engine.graphics.zones[1].flags;
912        for i in 0..10 {
913            // All points are still off-curve
914            assert!(!flags[i].is_on_curve());
915        }
916    }
917
918    #[test]
919    fn flip_range_on_off() {
920        let mut mock = MockEngine::new();
921        let mut engine = mock.engine();
922        // Points all start as off-curve in the mock engine.
923        // Flip 10..=20 on
924        engine.value_stack.push(10).unwrap();
925        engine.value_stack.push(20).unwrap();
926        engine.op_fliprgon().unwrap();
927        for (i, flag) in engine.graphics.zones[1].flags.iter().enumerate() {
928            assert_eq!(flag.is_on_curve(), (10..=20).contains(&i));
929        }
930        // Now flip 12..=15 off
931        engine.value_stack.push(12).unwrap();
932        engine.value_stack.push(15).unwrap();
933        engine.op_fliprgoff().unwrap();
934        for (i, flag) in engine.graphics.zones[1].flags.iter().enumerate() {
935            assert_eq!(
936                flag.is_on_curve(),
937                (10..=11).contains(&i) || (16..=20).contains(&i)
938            );
939        }
940    }
941
942    /// Backward compat + IUP state prevents flipping.
943    #[test]
944    fn state_prevents_flip_range_on_off() {
945        let mut mock = MockEngine::new();
946        let mut engine = mock.engine();
947        // Prevent flipping
948        engine.graphics.backward_compatibility = true;
949        engine.graphics.did_iup_x = true;
950        engine.graphics.did_iup_y = true;
951        // Points all start as off-curve in the mock engine.
952        // Try to flip 10..=20 on
953        engine.value_stack.push(10).unwrap();
954        engine.value_stack.push(20).unwrap();
955        engine.op_fliprgon().unwrap();
956        for flag in engine.graphics.zones[1].flags.iter() {
957            assert!(!flag.is_on_curve());
958        }
959        // Reset all points to on
960        for flag in engine.graphics.zones[1].flags.iter_mut() {
961            flag.set_on_curve();
962        }
963        // Now try to flip 12..=15 off
964        engine.value_stack.push(12).unwrap();
965        engine.value_stack.push(15).unwrap();
966        engine.op_fliprgoff().unwrap();
967        for flag in engine.graphics.zones[1].flags.iter() {
968            assert!(flag.is_on_curve());
969        }
970    }
971
972    #[test]
973    fn untouch_point() {
974        let mut mock = MockEngine::new();
975        let mut engine = mock.engine();
976        // Touch all points in both axes to start.
977        let count = engine.graphics.zones[1].points.len();
978        for i in 0..count {
979            engine.graphics.zones[1].touch(i, CoordAxis::Both).unwrap();
980        }
981        let mut untouch = |point_ix: usize, fx, fy, marker| {
982            assert!(engine.graphics.zp0().flags[point_ix].has_marker(marker));
983            // Untouch axis is based on freedom vector:
984            engine.graphics.freedom_vector.x = fx;
985            engine.graphics.freedom_vector.y = fy;
986            engine.value_stack.push(point_ix as i32).unwrap();
987            engine.op_utp().unwrap();
988            assert!(!engine.graphics.zp0().flags[point_ix].has_marker(marker));
989        };
990        // Untouch point 0 in x axis
991        untouch(0, 1, 0, PointMarker::TOUCHED_X);
992        // Untouch point 1 in y axis
993        untouch(1, 0, 1, PointMarker::TOUCHED_Y);
994        // untouch point 2 in both axes
995        untouch(2, 1, 1, PointMarker::TOUCHED);
996    }
997
998    #[test]
999    fn shp() {
1000        let mut mock = MockEngine::new();
1001        let mut engine = mock.engine();
1002        set_test_vectors(&mut engine);
1003        engine.graphics.backward_compatibility = false;
1004        engine.graphics.zp0 = ZonePointer::Glyph;
1005        engine.graphics.zp2 = ZonePointer::Glyph;
1006        engine.graphics.rp2 = 1;
1007        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1008        point.x = F26Dot6::from_bits(132);
1009        point.y = F26Dot6::from_bits(-256);
1010        engine.value_stack.push(1).unwrap();
1011        engine.op_shp(0).unwrap();
1012        let point = engine.graphics.zones[1].point(1).unwrap();
1013        assert_eq!(point.map(F26Dot6::to_bits), Point::new(136, -254));
1014    }
1015
1016    #[test]
1017    fn shc() {
1018        let mut mock = MockEngine::new();
1019        let mut engine = mock.engine();
1020        set_test_vectors(&mut engine);
1021        engine.graphics.backward_compatibility = false;
1022        engine.graphics.zp0 = ZonePointer::Glyph;
1023        engine.graphics.zp2 = ZonePointer::Glyph;
1024        engine.graphics.rp2 = 1;
1025        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1026        point.x = F26Dot6::from_bits(132);
1027        point.y = F26Dot6::from_bits(-256);
1028        engine.value_stack.push(0).unwrap();
1029        engine.op_shc(0).unwrap();
1030        let points = engine.graphics.zones[1]
1031            .points
1032            .iter()
1033            .map(|p| p.map(F26Dot6::to_bits))
1034            .take(3)
1035            .collect::<Vec<_>>();
1036        assert_eq!(
1037            points,
1038            &[Point::new(4, 2), Point::new(132, -256), Point::new(4, 2),]
1039        );
1040    }
1041
1042    #[test]
1043    fn shz() {
1044        let mut mock = MockEngine::new();
1045        let mut engine = mock.engine();
1046        set_test_vectors(&mut engine);
1047        engine.graphics.backward_compatibility = false;
1048        engine.graphics.zp0 = ZonePointer::Glyph;
1049        engine.graphics.zp2 = ZonePointer::Glyph;
1050        engine.graphics.rp2 = 1;
1051        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1052        point.x = F26Dot6::from_bits(132);
1053        point.y = F26Dot6::from_bits(-256);
1054        engine.value_stack.push(0).unwrap();
1055        engine.op_shz(0).unwrap();
1056        let points = engine.graphics.zones[1]
1057            .points
1058            .iter()
1059            .map(|p| p.map(F26Dot6::to_bits))
1060            .take(3)
1061            .collect::<Vec<_>>();
1062        assert_eq!(
1063            points,
1064            &[Point::new(4, 2), Point::new(132, -256), Point::new(4, 2),]
1065        );
1066    }
1067
1068    #[test]
1069    fn shpix() {
1070        let mut mock = MockEngine::new();
1071        let mut engine = mock.engine();
1072        set_test_vectors(&mut engine);
1073        engine.graphics.backward_compatibility = false;
1074        engine.graphics.zp2 = ZonePointer::Glyph;
1075        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1076        point.x = F26Dot6::from_bits(132);
1077        point.y = F26Dot6::from_bits(-256);
1078        // point index
1079        engine.value_stack.push(1).unwrap();
1080        // amount to move in pixels along freedom vector
1081        engine.value_stack.push(42).unwrap();
1082        engine.op_shpix().unwrap();
1083        let point = engine.graphics.zones[1].point(1).unwrap();
1084        assert_eq!(point.map(F26Dot6::to_bits), Point::new(170, -237));
1085    }
1086
1087    #[test]
1088    fn msirp() {
1089        let mut mock = MockEngine::new();
1090        let mut engine = mock.engine();
1091        set_test_vectors(&mut engine);
1092        engine.graphics.backward_compatibility = false;
1093        engine.graphics.zp0 = ZonePointer::Glyph;
1094        engine.graphics.zp1 = ZonePointer::Glyph;
1095        let point = engine.graphics.zones[1].point_mut(1).unwrap();
1096        point.x = F26Dot6::from_bits(132);
1097        point.y = F26Dot6::from_bits(-256);
1098        // point index
1099        engine.value_stack.push(1).unwrap();
1100        // amount to move in pixels along freedom vector
1101        engine.value_stack.push(-42).unwrap();
1102        engine.op_msirp(0).unwrap();
1103        let point = engine.graphics.zones[1].point(1).unwrap();
1104        assert_eq!(point.map(F26Dot6::to_bits), Point::new(91, -277));
1105        assert_eq!(engine.graphics.rp0, 0);
1106        // opcode with bit 0 set changes rp0 to point_ix
1107        engine.value_stack.push(4).unwrap();
1108        engine.value_stack.push(0).unwrap();
1109        engine.op_msirp(1).unwrap();
1110        assert_eq!(engine.graphics.rp0, 4);
1111    }
1112
1113    #[test]
1114    fn mdap() {
1115        let mut mock = MockEngine::new();
1116        let mut engine = mock.engine();
1117        set_test_vectors(&mut engine);
1118        engine.graphics.backward_compatibility = false;
1119        engine.graphics.zp0 = ZonePointer::Glyph;
1120        // with rounding
1121        engine.set_point_f26dot6(1, 1, (132, -256));
1122        engine.value_stack.push(1).unwrap();
1123        engine.op_mdap(1).unwrap();
1124        let point = engine.graphics.zones[1].point(1).unwrap();
1125        assert_eq!(point.map(F26Dot6::to_bits), Point::new(128, -258));
1126        // without rounding
1127        engine.set_point_f26dot6(1, 2, (132, -256));
1128        engine.value_stack.push(2).unwrap();
1129        engine.op_mdap(0).unwrap();
1130        let point = engine.graphics.zones[1].point(2).unwrap();
1131        assert_eq!(point.map(F26Dot6::to_bits), Point::new(132, -256));
1132    }
1133
1134    #[test]
1135    fn miap() {
1136        let mut mock = MockEngine::new();
1137        let mut engine = mock.engine();
1138        set_test_vectors(&mut engine);
1139        engine.graphics.backward_compatibility = false;
1140        engine.graphics.zp0 = ZonePointer::Glyph;
1141        // set a CVT distance
1142        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1143        // with rounding
1144        engine.set_point_f26dot6(1, 1, (132, -256));
1145        engine.value_stack.push(1).unwrap();
1146        engine.value_stack.push(1).unwrap();
1147        engine.op_miap(1).unwrap();
1148        let point = engine.graphics.zones[1].point(1).unwrap();
1149        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1150        // without rounding
1151        engine.set_point_f26dot6(1, 2, (132, -256));
1152        engine.value_stack.push(2).unwrap();
1153        engine.value_stack.push(1).unwrap();
1154        engine.op_miap(0).unwrap();
1155        let point = engine.graphics.zones[1].point(2).unwrap();
1156        assert_eq!(point.map(F26Dot6::to_bits), Point::new(171, -236));
1157    }
1158
1159    /// Tests bit 'a' of MDRP which just sets rp0 to the adjusted point
1160    /// after move.
1161    #[test]
1162    fn mdrp_rp0() {
1163        let mut mock = MockEngine::new();
1164        let mut engine = mock.engine();
1165        engine.graphics.rp0 = 0;
1166        // Don't change rp0
1167        engine.value_stack.push(1).unwrap();
1168        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1169        assert_eq!(engine.graphics.rp0, 0);
1170        // Change rp0
1171        engine.value_stack.push(1).unwrap();
1172        engine.op_mdrp(Opcode::MDRP10000 as _).unwrap();
1173        assert_eq!(engine.graphics.rp0, 1);
1174    }
1175
1176    /// Test bit "b" which controls whether distances are adjusted
1177    /// to the minimum_distance field of GraphicsState.
1178    #[test]
1179    fn mdrp_mindist() {
1180        let mut mock = MockEngine::new();
1181        let mut engine = mock.engine();
1182        set_test_vectors(&mut engine);
1183        engine.graphics.backward_compatibility = false;
1184        engine.graphics.zp0 = ZonePointer::Glyph;
1185        // without min distance check
1186        engine.set_point_f26dot6(1, 1, (132, -256));
1187        engine.value_stack.push(1).unwrap();
1188        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1189        let point = engine.graphics.zones[1].point(1).unwrap();
1190        assert_eq!(point.map(F26Dot6::to_bits), Point::new(128, -258));
1191        // with min distance check
1192        engine.set_point_f26dot6(1, 2, (132, -256));
1193        engine.value_stack.push(2).unwrap();
1194        engine.op_mdrp(Opcode::MDRP01000 as _).unwrap();
1195        let point = engine.graphics.zones[1].point(2).unwrap();
1196        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1197    }
1198
1199    /// Test bit "c" which controls whether distances are rounded.
1200    #[test]
1201    fn mdrp_round() {
1202        let mut mock = MockEngine::new();
1203        let mut engine = mock.engine();
1204        set_test_vectors(&mut engine);
1205        engine.graphics.backward_compatibility = false;
1206        engine.graphics.zp0 = ZonePointer::Glyph;
1207        engine.op_rthg().unwrap();
1208        // without rounding
1209        engine.set_point_f26dot6(1, 1, (132, -231));
1210        engine.value_stack.push(1).unwrap();
1211        engine.op_mdrp(Opcode::MDRP00000 as _).unwrap();
1212        let point = engine.graphics.zones[1].point(1).unwrap();
1213        assert_eq!(point.map(F26Dot6::to_bits), Point::new(119, -238));
1214        // with rounding
1215        engine.set_point_f26dot6(1, 2, (132, -231));
1216        engine.value_stack.push(2).unwrap();
1217        engine.op_mdrp(Opcode::MDRP00100 as _).unwrap();
1218        let point = engine.graphics.zones[1].point(2).unwrap();
1219        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1220    }
1221
1222    /// Tests bit 'a' of MIRP which just sets rp0 to the adjusted point
1223    /// after move.
1224    #[test]
1225    fn mirp_rp0() {
1226        let mut mock = MockEngine::new();
1227        let mut engine = mock.engine();
1228        engine.graphics.rp0 = 0;
1229        // Don't change rp0
1230        engine.value_stack.push(1).unwrap();
1231        engine.value_stack.push(1).unwrap();
1232        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1233        assert_eq!(engine.graphics.rp0, 0);
1234        // Change rp0
1235        engine.value_stack.push(1).unwrap();
1236        engine.value_stack.push(1).unwrap();
1237        engine.op_mirp(Opcode::MIRP10000 as _).unwrap();
1238        assert_eq!(engine.graphics.rp0, 1);
1239    }
1240
1241    /// Test bit "b" which controls whether distances are adjusted
1242    /// to the minimum_distance field of GraphicsState.
1243    #[test]
1244    fn mirp_mindist() {
1245        let mut mock = MockEngine::new();
1246        let mut engine = mock.engine();
1247        set_test_vectors(&mut engine);
1248        engine.graphics.backward_compatibility = false;
1249        engine.graphics.zp0 = ZonePointer::Glyph;
1250        // set a CVT distance
1251        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1252        // without min distance check
1253        engine.set_point_f26dot6(1, 1, (132, -256));
1254        engine.value_stack.push(1).unwrap();
1255        engine.value_stack.push(1).unwrap();
1256        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1257        let point = engine.graphics.zones[1].point(1).unwrap();
1258        assert_eq!(point.map(F26Dot6::to_bits), Point::new(171, -236));
1259        // with min distance check
1260        engine.set_point_f26dot6(1, 2, (132, -256));
1261        engine.value_stack.push(2).unwrap();
1262        engine.value_stack.push(1).unwrap();
1263        engine.op_mirp(Opcode::MIRP01000 as _).unwrap();
1264        let point = engine.graphics.zones[1].point(2).unwrap();
1265        assert_eq!(point.map(F26Dot6::to_bits), Point::new(186, -229));
1266    }
1267
1268    /// Test bit "c" which controls whether distances are rounded.
1269    #[test]
1270    fn mirp_round() {
1271        let mut mock = MockEngine::new();
1272        let mut engine = mock.engine();
1273        set_test_vectors(&mut engine);
1274        engine.graphics.backward_compatibility = false;
1275        engine.graphics.zp0 = ZonePointer::Glyph;
1276        // set a CVT distance
1277        engine.cvt.set(1, F26Dot6::from_f64(0.75)).unwrap();
1278        engine.op_rthg().unwrap();
1279        // without rounding
1280        engine.set_point_f26dot6(1, 1, (132, -231));
1281        engine.value_stack.push(1).unwrap();
1282        engine.value_stack.push(1).unwrap();
1283        engine.op_mirp(Opcode::MIRP00000 as _).unwrap();
1284        let point = engine.graphics.zones[1].point(1).unwrap();
1285        assert_eq!(point.map(F26Dot6::to_bits), Point::new(162, -216));
1286        // with rounding
1287        engine.set_point_f26dot6(1, 2, (132, -231));
1288        engine.value_stack.push(2).unwrap();
1289        engine.value_stack.push(1).unwrap();
1290        engine.op_mirp(Opcode::MIRP00100 as _).unwrap();
1291        let point = engine.graphics.zones[1].point(2).unwrap();
1292        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1293    }
1294
1295    #[test]
1296    fn alignrp() {
1297        let mut mock = MockEngine::new();
1298        let mut engine = mock.engine();
1299        set_test_vectors(&mut engine);
1300        engine.graphics.backward_compatibility = false;
1301        engine.graphics.zp0 = ZonePointer::Glyph;
1302        engine.graphics.zp1 = ZonePointer::Glyph;
1303        engine.graphics.rp0 = 0;
1304        engine.set_point_f26dot6(1, 0, (132, -231));
1305        engine.set_point_f26dot6(1, 1, (-72, 109));
1306        engine.value_stack.push(1).unwrap();
1307        engine.op_alignrp().unwrap();
1308        let point = engine.graphics.zones[1].point(1).unwrap();
1309        assert_eq!(point.map(F26Dot6::to_bits), Point::new(-45, 122));
1310    }
1311
1312    #[test]
1313    fn isect() {
1314        let mut mock = MockEngine::new();
1315        let mut engine = mock.engine();
1316        engine.graphics.zp0 = ZonePointer::Glyph;
1317        engine.graphics.zp1 = ZonePointer::Glyph;
1318        engine.graphics.rp0 = 0;
1319        // Two points for line 1
1320        engine.set_point_f26dot6(1, 0, (0, 0));
1321        engine.set_point_f26dot6(1, 1, (100, 100));
1322        // And two more for line 2
1323        engine.set_point_f26dot6(1, 2, (0, 100));
1324        engine.set_point_f26dot6(1, 3, (100, 0));
1325        // Push point numbers: first is the point where the
1326        // intersection should be stored.
1327        for ix in [4, 0, 1, 2, 3] {
1328            engine.value_stack.push(ix).unwrap();
1329        }
1330        engine.op_isect().unwrap();
1331        let point = engine.graphics.zones[1].point(4).unwrap();
1332        assert_eq!(point.map(F26Dot6::to_bits), Point::new(50, 50));
1333    }
1334
1335    #[test]
1336    fn alignpts() {
1337        let mut mock = MockEngine::new();
1338        let mut engine = mock.engine();
1339        set_test_vectors(&mut engine);
1340        engine.graphics.backward_compatibility = false;
1341        engine.graphics.zp0 = ZonePointer::Glyph;
1342        engine.graphics.zp1 = ZonePointer::Glyph;
1343        engine.set_point_f26dot6(1, 0, (132, -231));
1344        engine.set_point_f26dot6(1, 1, (-72, 109));
1345        engine.value_stack.push(0).unwrap();
1346        engine.value_stack.push(1).unwrap();
1347        engine.op_alignpts().unwrap();
1348        let p1 = engine.graphics.zones[1].point(0).unwrap();
1349        let p2 = engine.graphics.zones[1].point(1).unwrap();
1350        assert_eq!(p1.map(F26Dot6::to_bits), Point::new(119, -238));
1351        assert_eq!(p2.map(F26Dot6::to_bits), Point::new(-59, 116));
1352    }
1353
1354    #[test]
1355    fn ip() {
1356        let mut mock = MockEngine::new();
1357        let mut engine = mock.engine();
1358        set_test_vectors(&mut engine);
1359        engine.graphics.backward_compatibility = false;
1360        engine.graphics.zp0 = ZonePointer::Glyph;
1361        engine.graphics.zp1 = ZonePointer::Glyph;
1362        engine.graphics.zp2 = ZonePointer::Glyph;
1363        engine.graphics.rp1 = 2;
1364        engine.graphics.rp2 = 3;
1365        engine.set_point_f26dot6(1, 2, (72, -109));
1366        engine.set_point_f26dot6(1, 1, (132, -231));
1367        engine.value_stack.push(1).unwrap();
1368        engine.op_ip().unwrap();
1369        let point = engine.graphics.zones[1].point(1).unwrap();
1370        assert_eq!(point.map(F26Dot6::to_bits), Point::new(147, -223));
1371    }
1372
1373    #[test]
1374    fn iup_flags() {
1375        // IUP shift and interpolate logic is tested in ../zone.rs so just
1376        // check the flags here.
1377        let mut mock = MockEngine::new();
1378        let mut engine = mock.engine();
1379        assert!(!engine.graphics.did_iup_x);
1380        assert!(!engine.graphics.did_iup_y);
1381        // IUP[y]
1382        engine.op_iup(0).unwrap();
1383        assert!(!engine.graphics.did_iup_x);
1384        assert!(engine.graphics.did_iup_y);
1385        // IUP[x]
1386        engine.op_iup(1).unwrap();
1387        assert!(engine.graphics.did_iup_x);
1388        assert!(engine.graphics.did_iup_y);
1389    }
1390
1391    // Add with overflow caught by fuzzer:
1392    // https://issues.oss-fuzz.com/issues/377736138
1393    #[test]
1394    fn flip_region_avoid_overflow() {
1395        let mut mock = MockEngine::new();
1396        let mut engine = mock.engine();
1397        engine.value_stack.push(1).unwrap();
1398        engine.value_stack.push(-1).unwrap();
1399        // Just don't panic
1400        let _ = engine.set_on_curve_for_range(true);
1401    }
1402
1403    fn set_test_vectors(engine: &mut Engine) {
1404        let v = math::normalize14(100, 50);
1405        engine.graphics.proj_vector = v;
1406        engine.graphics.dual_proj_vector = v;
1407        engine.graphics.freedom_vector = v;
1408        engine.graphics.update_projection_state();
1409    }
1410
1411    impl Engine<'_> {
1412        fn set_point_f26dot6(&mut self, zone_ix: usize, point_ix: usize, xy: (i32, i32)) {
1413            let p = self.graphics.zones[zone_ix].point_mut(point_ix).unwrap();
1414            p.x = F26Dot6::from_bits(xy.0);
1415            p.y = F26Dot6::from_bits(xy.1);
1416        }
1417    }
1418}