Skip to main content

read_fonts/tables/glyf/outline/
scale.rs

1//! Coordinate representation and rounding policy.
2//!
3//! [`Scale`] holds the arithmetic that differs between target coordinate
4//! types. Loading takes the same steps whichever one is in use.
5//!
6//! Three are provided:
7//!
8//! - [`Scale26Dot6`] matches FreeType: 26.6 coordinates, 16.16 deltas, and
9//!   rounding where FreeType rounds.
10//! - [`ScaleF32`] matches HarfBuzz: floating point throughout, no rounding.
11//! - [`Unscaled`] stays in font units, which is what an autohinter needs. It
12//!   matches [`Scale26Dot6`] at no size, without the round trip through 26.6.
13
14use crate::model::Error;
15use bytemuck::NoUninit;
16
17use crate::{
18    tables::{
19        glyf::{PointCoord, PointFlags, SimpleGlyph, Transform, PHANTOM_POINT_COUNT},
20        gvar::{DeltaBuffers, GlyphVariationData},
21    },
22    types::{F26Dot6, F2Dot14, Fixed, Point},
23    ReadError,
24};
25
26/// How coordinates are represented and rounded while loading an outline.
27///
28/// This trait is sealed. [`load_simple_points`](Self::load_simple_points)
29/// takes a type this crate does not export, so the implementations here are
30/// the only ones.
31pub trait Scale: Sized {
32    /// Coordinate type of the loaded outline.
33    type Coord: PointCoord + NoUninit;
34    /// Accumulator type for variation deltas.
35    type Delta: PointCoord + NoUninit;
36
37    /// True if the scale keeps a parallel copy of the points in font units,
38    /// which hinting needs and which sparse delta interpolation uses as its
39    /// reference coordinates.
40    const NEEDS_UNSCALED: bool;
41
42    /// True if a size was supplied. When false the outline comes back in font
43    /// units, expressed in [`Self::Coord`].
44    fn is_scaled(&self) -> bool;
45
46    // -- phantom points, which hold font units until they are scaled --------
47
48    /// Converts a font unit value into the representation phantom points use
49    /// before they are scaled.
50    fn phantom_from_font_units(&self, value: i32) -> Self::Coord;
51
52    /// Reinterprets an output coordinate as the integer a hinter's font unit
53    /// buffer holds.
54    ///
55    /// A composite has no font unit points of its own, so FreeType fills that
56    /// buffer with the loaded coordinates as they stand. For [`Scale26Dot6`]
57    /// that means raw fixed point bits rather than font units, which is what
58    /// the interpreter expects.
59    fn coord_to_hinting_raw(&self, value: Self::Coord) -> i32;
60
61    /// Scales a phantom point that still holds font units.
62    fn scale_phantom(&self, point: Point<Self::Coord>) -> Point<Self::Coord>;
63
64    /// Adds a composite glyph's phantom point delta.
65    fn add_phantom_delta(
66        &self,
67        point: Point<Self::Coord>,
68        delta: Point<Self::Delta>,
69    ) -> Point<Self::Coord>;
70
71    /// Rounds a phantom point, which FreeType does before hinting.
72    fn round_phantom(&self, point: Point<Self::Coord>) -> Point<Self::Coord>;
73
74    // -- whole passes, where the modes differ in more than arithmetic -------
75
76    /// Sets the phantom points of a glyph with no outline.
77    ///
78    /// `deltas` are this glyph's phantom point deltas, if the font is variable
79    /// and has them. They apply whether or not the font has `HVAR`: the
80    /// phantom points are seeded from unvaried metrics, so `gvar` is their
81    /// only source of variation.
82    fn load_empty_phantom(
83        &self,
84        phantom: &mut [Point<Self::Coord>; PHANTOM_POINT_COUNT],
85        deltas: Option<[Point<Fixed>; PHANTOM_POINT_COUNT]>,
86    );
87
88    /// Reads a simple glyph's points, applies variation deltas, appends the
89    /// phantom points and writes the scaled result.
90    ///
91    /// Returns the phantom points as they ended up, which the caller carries
92    /// forward.
93    fn load_simple_points(
94        &self,
95        pass: SimplePass<'_, '_, Self>,
96    ) -> Result<[Point<Self::Coord>; PHANTOM_POINT_COUNT], Error>;
97
98    // -- composite components ----------------------------------------------
99
100    /// Applies a component's 2x2 transform to points that have already been
101    /// placed.
102    ///
103    /// Takes the whole slice so a scale converts the transform once rather
104    /// than once per point.
105    fn transform_points(&self, points: &mut [Point<Self::Coord>], transform: &Transform);
106
107    /// Computes the translation for a component given as an offset.
108    ///
109    /// `scale_offset` is set when the component carries
110    /// `SCALED_COMPONENT_OFFSET` without `UNSCALED_COMPONENT_OFFSET`,
111    /// `delta` is the component's variation delta if there is one, and
112    /// `rounding` selects the axes that are rounded to the pixel grid. No
113    /// axis is rounded unless the component carries `ROUND_XY_TO_GRID` and
114    /// the outline is being hinted.
115    fn component_offset(
116        &self,
117        offset: Point<i32>,
118        transform: &Transform,
119        scale_offset: bool,
120        delta: Option<Point<Self::Delta>>,
121        rounding: OffsetRounding,
122    ) -> Point<Self::Coord>;
123}
124
125/// The axes of a composite component's offset that are rounded to the pixel
126/// grid.
127///
128/// This applies when the component has the `ROUND_XY_TO_GRID` flag. The
129/// hinter selects the axes through
130/// [`Hinter::round_composite_x_offset`](super::Hinter::round_composite_x_offset).
131#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
132pub struct OffsetRounding {
133    pub x: bool,
134    pub y: bool,
135}
136
137/// Everything [`Scale::load_simple_points`] needs beyond the glyph itself.
138pub struct SimplePass<'a, 'v, S: Scale> {
139    /// The glyph being read.
140    pub glyph: &'a SimpleGlyph<'a>,
141    /// Output coordinates, sized for the glyph's points plus phantom points.
142    pub scaled: &'a mut [Point<S::Coord>],
143    /// Font unit coordinates, empty unless [`Scale::NEEDS_UNSCALED`].
144    pub unscaled: &'a mut [Point<i32>],
145    /// Point flags, the same length as `scaled`.
146    pub flags: &'a mut [PointFlags],
147    /// Contour end points, already read and relative to this glyph.
148    pub contours: &'a [u16],
149    /// The phantom points to append, in font units.
150    pub phantom: [Point<S::Coord>; PHANTOM_POINT_COUNT],
151    /// Delta and interpolation buffers, empty for a static instance.
152    pub deltas: DeltaBuffers<'a, S::Delta>,
153    /// The variation data to apply, or `None` if this glyph has none.
154    ///
155    /// Has its own lifetime because it is invariant and cannot shrink to
156    /// match the borrows above.
157    pub var_data: Option<GlyphVariationData<'v>>,
158    /// True if the font varies, whatever this glyph has.
159    ///
160    /// A glyph with no variation data still takes the delta path, on zeroed
161    /// deltas, because the two paths round differently.
162    pub varies: bool,
163    /// Normalized variation coordinates, empty for a static instance.
164    pub coords: &'a [F2Dot14],
165    /// Cached shared tuple scalars at `coords`.
166    pub gvar_scalars: &'a [Fixed],
167    /// True if the font has `HVAR`, which changes how FreeType rounds the
168    /// deltas it applies to phantom points.
169    pub has_hvar: bool,
170    /// True if the loaded outline will be hinted.
171    pub is_hinted: bool,
172}
173
174// -------------------------------------------------------------------------
175// FreeType: 26.6 coordinates, 16.16 deltas
176// -------------------------------------------------------------------------
177
178/// Scales font units to 26.6 fixed point, reproducing FreeType's arithmetic.
179#[derive(Copy, Clone, Debug)]
180pub struct Scale26Dot6 {
181    scale: Fixed,
182    is_scaled: bool,
183}
184
185impl Scale26Dot6 {
186    /// Creates a scale for the given size in pixels per em.
187    ///
188    /// `None` produces an unscaled outline with coordinates expressed in 26.6.
189    pub fn new(ppem: Option<f32>, units_per_em: u16) -> Self {
190        if let Some(ppem) = ppem {
191            if units_per_em > 0 {
192                return Self {
193                    scale: Fixed::from_bits((ppem * 64.) as i32)
194                        / Fixed::from_bits(units_per_em as i32),
195                    is_scaled: true,
196                };
197            }
198        }
199        Self {
200            scale: Fixed::from_bits(0x10000),
201            is_scaled: false,
202        }
203    }
204
205    /// The scale factor that converts font units to scaled 26.6, in 16.16.
206    pub fn to_bits(self) -> i32 {
207        self.scale.to_bits()
208    }
209
210    /// Multiplies a raw value, in font units or 26.6, by the scale factor.
211    ///
212    /// The factor converts as it scales, so this serves both cases.
213    fn mul(&self, value: i32) -> F26Dot6 {
214        F26Dot6::from_bits((Fixed::from_bits(value) * self.scale).to_bits())
215    }
216
217    fn mul_point(&self, value: Point<i32>) -> Point<F26Dot6> {
218        Point::new(self.mul(value.x), self.mul(value.y))
219    }
220
221    /// Scales a point in font units that carries a delta, given in 26.6.
222    ///
223    /// The scale factor has an i32 to 26.6 conversion built into it, so the
224    /// product is shifted back down afterwards.
225    fn mul_point_with_delta(
226        &self,
227        font_units: Point<i32>,
228        delta: Point<F26Dot6>,
229    ) -> Point<F26Dot6> {
230        let scaled =
231            self.mul_point((font_units.map(F26Dot6::from_i32) + delta).map(|v| v.to_bits()));
232        scaled.map(|v| F26Dot6::from_bits(v.to_i32()))
233    }
234}
235
236impl Scale for Scale26Dot6 {
237    type Coord = F26Dot6;
238    type Delta = Fixed;
239
240    const NEEDS_UNSCALED: bool = true;
241
242    fn is_scaled(&self) -> bool {
243        self.is_scaled
244    }
245
246    fn phantom_from_font_units(&self, value: i32) -> F26Dot6 {
247        F26Dot6::from_bits(value)
248    }
249
250    fn coord_to_hinting_raw(&self, value: F26Dot6) -> i32 {
251        value.to_bits()
252    }
253
254    fn scale_phantom(&self, point: Point<F26Dot6>) -> Point<F26Dot6> {
255        if self.is_scaled {
256            self.mul_point(point.map(|v| v.to_bits()))
257        } else {
258            point.map(|v| F26Dot6::from_i32(v.to_bits()))
259        }
260    }
261
262    fn add_phantom_delta(&self, point: Point<F26Dot6>, delta: Point<Fixed>) -> Point<F26Dot6> {
263        // FreeType rounds off the fractional part of a composite's deltas.
264        point + delta.map(Fixed::to_i32).map(F26Dot6::from_bits)
265    }
266
267    fn round_phantom(&self, point: Point<F26Dot6>) -> Point<F26Dot6> {
268        point.map(|v| v.round())
269    }
270
271    fn load_empty_phantom(
272        &self,
273        phantom: &mut [Point<F26Dot6>; PHANTOM_POINT_COUNT],
274        deltas: Option<[Point<Fixed>; PHANTOM_POINT_COUNT]>,
275    ) {
276        let mut font_units = phantom.map(|point| point.map(|v| v.to_bits()));
277        if let Some(deltas) = deltas {
278            font_units[0] += deltas[0].map(Fixed::to_i32);
279            font_units[1] += deltas[1].map(Fixed::to_i32);
280        }
281        for (out, font_units) in phantom.iter_mut().zip(&font_units) {
282            *out = if self.is_scaled {
283                self.mul_point(*font_units)
284            } else {
285                font_units.map(F26Dot6::from_i32)
286            };
287        }
288    }
289
290    fn load_simple_points(
291        &self,
292        pass: SimplePass<'_, '_, Self>,
293    ) -> Result<[Point<F26Dot6>; PHANTOM_POINT_COUNT], Error> {
294        let SimplePass {
295            glyph,
296            scaled,
297            unscaled,
298            flags,
299            contours,
300            phantom,
301            mut deltas,
302            var_data,
303            varies,
304            coords,
305            gvar_scalars,
306            has_hvar,
307            is_hinted,
308        } = pass;
309        let phantom_start = glyph.num_points();
310        // Phantom points hold font units in the low bits until they are scaled.
311        let raw_phantom = phantom.map(|point| point.map(|v| v.to_bits()));
312        read_points_and_phantom(glyph, unscaled, flags, &raw_phantom)?;
313        let have_deltas = varies
314            && simple_deltas(
315                var_data,
316                (coords, gvar_scalars),
317                unscaled,
318                flags,
319                contours,
320                &mut deltas,
321            );
322        if self.is_scaled {
323            if have_deltas {
324                for ((point, unscaled), delta) in scaled
325                    .iter_mut()
326                    .zip(unscaled.iter())
327                    .zip(deltas.deltas.iter())
328                {
329                    *point = self.mul_point_with_delta(*unscaled, delta.map(Fixed::to_f26dot6));
330                }
331                // FreeType applies different rounding to HVAR deltas. Since we
332                // only use gvar, mimic that for phantom point deltas when an
333                // HVAR table is present.
334                if has_hvar {
335                    for ((point, unscaled), delta) in scaled[phantom_start..]
336                        .iter_mut()
337                        .zip(&unscaled[phantom_start..])
338                        .zip(&deltas.deltas[phantom_start..])
339                    {
340                        // Whole font units, rather than the 26.6 above.
341                        let delta = delta.map(Fixed::to_i32).map(F26Dot6::from_i32);
342                        *point = self.mul_point_with_delta(*unscaled, delta);
343                    }
344                }
345                if is_hinted {
346                    // Hinting reads the font unit points, so those need the
347                    // deltas as well.
348                    fold_deltas_into_font_units(unscaled, deltas.deltas);
349                }
350            } else {
351                for (point, unscaled) in scaled.iter_mut().zip(unscaled.iter()) {
352                    *point = self.mul_point(*unscaled);
353                }
354            }
355        } else {
356            if have_deltas {
357                fold_deltas_into_font_units(unscaled, deltas.deltas);
358            }
359            // Unlike FreeType, we also return unscaled outlines in 26.6.
360            for (point, unscaled) in scaled.iter_mut().zip(unscaled.iter()) {
361                *point = unscaled.map(F26Dot6::from_i32);
362            }
363        }
364        let mut out = [Point::default(); PHANTOM_POINT_COUNT];
365        out.copy_from_slice(&scaled[phantom_start..]);
366        Ok(out)
367    }
368
369    fn transform_points(&self, points: &mut [Point<F26Dot6>], transform: &Transform) {
370        let t = FixedTransform::new(transform);
371        if self.is_scaled {
372            for point in points.iter_mut() {
373                let p = point.map(|c| Fixed::from_bits(c.to_bits()));
374                *point = Point::new(p.x * t.xx + p.y * t.xy, p.x * t.yx + p.y * t.yy)
375                    .map(|c| F26Dot6::from_bits(c.to_bits()));
376            }
377        } else {
378            // This juggling is necessary because, unlike FreeType, we also
379            // return unscaled outlines in 26.6 format.
380            for point in points.iter_mut() {
381                let p = point.map(|c| Fixed::from_bits(c.to_i32()));
382                *point = Point::new(p.x * t.xx + p.y * t.xy, p.x * t.yx + p.y * t.yy)
383                    .map(|c| F26Dot6::from_i32(c.to_bits()));
384            }
385        }
386    }
387
388    fn component_offset(
389        &self,
390        offset: Point<i32>,
391        transform: &Transform,
392        scale_offset: bool,
393        delta: Option<Point<Fixed>>,
394        rounding: OffsetRounding,
395    ) -> Point<F26Dot6> {
396        let (mut x, mut y) = (offset.x, offset.y);
397        if scale_offset {
398            let t = FixedTransform::new(transform);
399            // FreeType uses a fixed point multiplication here. Scale x by the
400            // magnitude of the x basis and y by the y basis.
401            x = (Fixed::from_bits(x) * hypot_fixed(t.xx, t.xy)).to_bits();
402            y = (Fixed::from_bits(y) * hypot_fixed(t.yy, t.yx)).to_bits();
403        }
404        if let Some(delta) = delta {
405            // For composite glyphs, we copy FreeType and round off the
406            // fractional parts of deltas.
407            x += delta.x.to_i32();
408            y += delta.y.to_i32();
409        }
410        if self.is_scaled {
411            let mut offset = self.mul_point(Point::new(x, y));
412            if rounding.x {
413                offset.x = offset.x.round();
414            }
415            if rounding.y {
416                offset.y = offset.y.round();
417            }
418            offset
419        } else {
420            Point::new(x, y).map(F26Dot6::from_i32)
421        }
422    }
423}
424
425// -------------------------------------------------------------------------
426// HarfBuzz: floating point throughout
427// -------------------------------------------------------------------------
428
429/// Scales font units to floating point pixels, reproducing HarfBuzz's
430/// arithmetic.
431#[derive(Copy, Clone, Debug)]
432pub struct ScaleF32 {
433    scale: f32,
434}
435
436impl ScaleF32 {
437    /// Creates a scale for the given size in pixels per em.
438    pub fn new(ppem: Option<f32>, units_per_em: u16) -> Self {
439        let scale = if units_per_em == 0 {
440            1.0
441        } else {
442            ppem.map(|ppem| ppem / units_per_em as f32).unwrap_or(1.0)
443        };
444        Self { scale }
445    }
446}
447
448impl Scale for ScaleF32 {
449    type Coord = f32;
450    type Delta = f32;
451
452    const NEEDS_UNSCALED: bool = false;
453
454    fn is_scaled(&self) -> bool {
455        self.scale != 1.0
456    }
457
458    fn phantom_from_font_units(&self, value: i32) -> f32 {
459        value as f32
460    }
461
462    fn coord_to_hinting_raw(&self, value: f32) -> i32 {
463        value as i32
464    }
465
466    fn scale_phantom(&self, point: Point<f32>) -> Point<f32> {
467        if self.scale != 1.0 {
468            point * self.scale
469        } else {
470            point
471        }
472    }
473
474    fn add_phantom_delta(&self, point: Point<f32>, delta: Point<f32>) -> Point<f32> {
475        point + delta
476    }
477
478    fn round_phantom(&self, point: Point<f32>) -> Point<f32> {
479        point
480    }
481
482    fn load_empty_phantom(
483        &self,
484        phantom: &mut [Point<f32>; PHANTOM_POINT_COUNT],
485        deltas: Option<[Point<Fixed>; PHANTOM_POINT_COUNT]>,
486    ) {
487        let mut font_units = *phantom;
488        // HarfBuzz has no equivalent of this, so it follows FreeType.
489        if let Some(deltas) = deltas {
490            font_units[0] += deltas[0].map(Fixed::to_f32);
491            font_units[1] += deltas[1].map(Fixed::to_f32);
492        }
493        for (out, font_units) in phantom.iter_mut().zip(&font_units) {
494            *out = *font_units * self.scale;
495        }
496    }
497
498    fn load_simple_points(
499        &self,
500        pass: SimplePass<'_, '_, Self>,
501    ) -> Result<[Point<f32>; PHANTOM_POINT_COUNT], Error> {
502        let SimplePass {
503            glyph,
504            scaled,
505            flags,
506            contours,
507            phantom,
508            mut deltas,
509            var_data,
510            varies,
511            coords,
512            gvar_scalars,
513            ..
514        } = pass;
515        // `ScaleF32` works in place: the points start out as font
516        // units and are scaled at the end.
517        let phantom_start = glyph.num_points();
518        read_points_and_phantom(glyph, scaled, flags, &phantom)?;
519        if varies
520            && simple_deltas(
521                var_data,
522                (coords, gvar_scalars),
523                scaled,
524                flags,
525                contours,
526                &mut deltas,
527            )
528        {
529            for (point, delta) in scaled.iter_mut().zip(deltas.deltas.iter()) {
530                *point += *delta;
531            }
532        }
533        if self.scale != 1.0 {
534            for point in scaled.iter_mut() {
535                *point *= self.scale;
536            }
537        }
538        let mut out = [Point::default(); PHANTOM_POINT_COUNT];
539        out.copy_from_slice(&scaled[phantom_start..]);
540        Ok(out)
541    }
542
543    fn transform_points(&self, points: &mut [Point<f32>], transform: &Transform) {
544        let (xx, yx, xy, yy) = (
545            transform.xx.to_f32(),
546            transform.yx.to_f32(),
547            transform.xy.to_f32(),
548            transform.yy.to_f32(),
549        );
550        for point in points.iter_mut() {
551            *point = Point::new(point.x * xx + point.y * xy, point.x * yx + point.y * yy);
552        }
553    }
554
555    fn component_offset(
556        &self,
557        offset: Point<i32>,
558        transform: &Transform,
559        scale_offset: bool,
560        delta: Option<Point<f32>>,
561        _rounding: OffsetRounding,
562    ) -> Point<f32> {
563        let (mut x, mut y) = (offset.x as f32, offset.y as f32);
564        if scale_offset {
565            // FreeType implements hypot; we can just use the provided one.
566            fn hypot(a: f32, b: f32) -> f32 {
567                #[cfg(not(feature = "libm"))]
568                {
569                    a.hypot(b)
570                }
571                #[cfg(feature = "libm")]
572                {
573                    // Unused when `std` is also on, which is allowed: the
574                    // inherent method wins there. Matches `tables::varc`.
575                    #[allow(unused_imports)]
576                    use core_maths::CoreFloat;
577                    a.hypot(b)
578                }
579            }
580            // Scale x by the magnitude of the x-basis, y by the y-basis.
581            x *= hypot(transform.xx.to_f32(), transform.xy.to_f32());
582            y *= hypot(transform.yx.to_f32(), transform.yy.to_f32());
583        }
584        let mut offset = Point::new(x, y);
585        if let Some(delta) = delta {
586            // Composite deltas are in font units too, so this stays consistent.
587            offset += delta;
588        }
589        // The component's points have already been scaled, so the offset has to
590        // be as well.
591        offset * self.scale
592    }
593}
594
595// ---------------------------------------------------------------------------
596// Font units, unscaled
597// ---------------------------------------------------------------------------
598
599/// Leaves coordinates in font units.
600///
601/// Variations still apply, and composite components are still transformed and
602/// placed, but nothing is scaled or rounded to a pixel grid. This is what an
603/// autohinter wants: the outline as the designer drew it, at the requested
604/// location in variation space.
605///
606/// The arithmetic matches [`Scale26Dot6`] at no size, without the round trip
607/// through 26.6 that getting font units back out would otherwise need.
608#[derive(Copy, Clone, Debug, Default)]
609pub struct Unscaled;
610
611impl Scale for Unscaled {
612    type Coord = i32;
613    type Delta = Fixed;
614
615    // The output is already in font units, so there is nothing to keep a
616    // second copy of; the points serve as their own interpolation reference.
617    const NEEDS_UNSCALED: bool = false;
618
619    fn is_scaled(&self) -> bool {
620        false
621    }
622
623    fn phantom_from_font_units(&self, value: i32) -> i32 {
624        value
625    }
626
627    fn coord_to_hinting_raw(&self, value: i32) -> i32 {
628        value
629    }
630
631    fn scale_phantom(&self, point: Point<i32>) -> Point<i32> {
632        point
633    }
634
635    fn add_phantom_delta(&self, point: Point<i32>, delta: Point<Fixed>) -> Point<i32> {
636        point + delta.map(Fixed::to_i32)
637    }
638
639    fn round_phantom(&self, point: Point<i32>) -> Point<i32> {
640        point
641    }
642
643    fn load_empty_phantom(
644        &self,
645        phantom: &mut [Point<i32>; PHANTOM_POINT_COUNT],
646        deltas: Option<[Point<Fixed>; PHANTOM_POINT_COUNT]>,
647    ) {
648        if let Some(deltas) = deltas {
649            phantom[0] += deltas[0].map(Fixed::to_i32);
650            phantom[1] += deltas[1].map(Fixed::to_i32);
651        }
652    }
653
654    fn load_simple_points(
655        &self,
656        pass: SimplePass<'_, '_, Self>,
657    ) -> Result<[Point<i32>; PHANTOM_POINT_COUNT], Error> {
658        let SimplePass {
659            glyph,
660            scaled,
661            flags,
662            contours,
663            phantom,
664            mut deltas,
665            var_data,
666            varies,
667            coords,
668            gvar_scalars,
669            ..
670        } = pass;
671        // The points are their own font unit copy, so this works in place.
672        let phantom_start = glyph.num_points();
673        read_points_and_phantom(glyph, scaled, flags, &phantom)?;
674        let have_deltas = varies
675            && simple_deltas(
676                var_data,
677                (coords, gvar_scalars),
678                scaled,
679                flags,
680                contours,
681                &mut deltas,
682            );
683        if have_deltas {
684            fold_deltas_into_font_units(scaled, deltas.deltas);
685        }
686        let mut out = [Point::default(); PHANTOM_POINT_COUNT];
687        out.copy_from_slice(&scaled[phantom_start..]);
688        Ok(out)
689    }
690
691    fn transform_points(&self, points: &mut [Point<i32>], transform: &Transform) {
692        let t = FixedTransform::new(transform);
693        // Font units are treated as raw 16.16 for the multiply, which is what
694        // FreeType does for an unscaled outline.
695        for point in points.iter_mut() {
696            let p = point.map(Fixed::from_bits);
697            *point =
698                Point::new(p.x * t.xx + p.y * t.xy, p.x * t.yx + p.y * t.yy).map(|c| c.to_bits());
699        }
700    }
701
702    fn component_offset(
703        &self,
704        offset: Point<i32>,
705        transform: &Transform,
706        scale_offset: bool,
707        delta: Option<Point<Fixed>>,
708        _rounding: OffsetRounding,
709    ) -> Point<i32> {
710        let mut offset = offset;
711        if scale_offset {
712            let t = FixedTransform::new(transform);
713            offset = Point::new(
714                (Fixed::from_bits(offset.x) * hypot_fixed(t.xx, t.xy)).to_bits(),
715                (Fixed::from_bits(offset.y) * hypot_fixed(t.yy, t.yx)).to_bits(),
716            );
717        }
718        if let Some(delta) = delta {
719            offset += delta.map(Fixed::to_i32);
720        }
721        offset
722    }
723}
724
725// -------------------------------------------------------------------------
726// Shared helpers
727// -------------------------------------------------------------------------
728
729/// Reads a simple glyph's points into `dest` and appends the phantom points
730/// after them.
731///
732/// `dest` and `flags` cover the glyph's points plus the four phantom points.
733/// The staging type differs by scale -- font units for 26.6, the output
734/// coordinates themselves for the floating point one -- so this is generic over
735/// it, with the caller supplying phantom points already in that form.
736fn read_points_and_phantom<C: PointCoord>(
737    glyph: &SimpleGlyph,
738    dest: &mut [Point<C>],
739    flags: &mut [PointFlags],
740    phantom: &[Point<C>; PHANTOM_POINT_COUNT],
741) -> Result<(), ReadError> {
742    let point_count = glyph.num_points();
743    glyph.read_points_fast(&mut dest[..point_count], &mut flags[..point_count])?;
744    for (i, phantom) in phantom.iter().enumerate() {
745        dest[point_count + i] = *phantom;
746        flags[point_count + i] = Default::default();
747    }
748    Ok(())
749}
750
751/// Folds variation deltas into the font unit points, rounding them off the way
752/// FreeType does.
753fn fold_deltas_into_font_units(font_units: &mut [Point<i32>], deltas: &[Point<Fixed>]) {
754    for (point, delta) in font_units.iter_mut().zip(deltas) {
755        *point += delta.map(Fixed::to_i32);
756    }
757}
758
759/// A component's 2x2 transform in the 16.16 form FreeType uses.
760struct FixedTransform {
761    xx: Fixed,
762    yx: Fixed,
763    xy: Fixed,
764    yy: Fixed,
765}
766
767impl FixedTransform {
768    fn new(transform: &Transform) -> Self {
769        fn widen(x: F2Dot14) -> Fixed {
770            Fixed::from_bits(x.to_bits() as i32 * 4)
771        }
772        Self {
773            xx: widen(transform.xx),
774            yx: widen(transform.yx),
775            xy: widen(transform.xy),
776            yy: widen(transform.yy),
777        }
778    }
779}
780
781/// Approximate magnitude of a transform basis vector.
782///
783/// According to FreeType this algorithm is a "guess" that works better than the
784/// one Apple documents, so both fixed point modes use it to scale a component
785/// offset that asks to be scaled.
786///
787/// <https://github.com/freetype/freetype/blob/b1c90733ee6a04882b133101d61b12e352eeb290/src/truetype/ttgload.c#L1259>
788fn hypot_fixed(a: Fixed, b: Fixed) -> Fixed {
789    let a = a.to_bits().abs();
790    let b = b.to_bits().abs();
791    Fixed::from_bits(if a > b {
792        a + ((3 * b) >> 3)
793    } else {
794        b + ((3 * a) >> 3)
795    })
796}
797
798/// Computes the deltas for a simple glyph of a varying font, and reports
799/// whether the caller should take its delta path.
800///
801/// The glyph takes that path even with no variation data of its own, on
802/// zeroed deltas. That looks wasteful and is not: [`Scale26Dot6`]
803/// scales `(font units + delta)` and shifts back down, which does not round
804/// the same way as scaling the font units alone, so a whole 26.6 unit rides on
805/// which path runs. FreeType makes the same distinction.
806fn simple_deltas<C, D>(
807    var_data: Option<GlyphVariationData<'_>>,
808    (coords, gvar_scalars): (&[F2Dot14], &[Fixed]),
809    points: &[Point<C>],
810    flags: &mut [PointFlags],
811    contours: &[u16],
812    buffers: &mut DeltaBuffers<'_, D>,
813) -> bool
814where
815    C: PointCoord,
816    D: PointCoord + From<C>,
817{
818    match var_data {
819        Some(var_data) => var_data
820            .simple_deltas_with_scalars(coords, gvar_scalars, points, flags, contours, buffers)
821            .is_some(),
822        None => {
823            for delta in buffers.deltas.iter_mut() {
824                *delta = Default::default();
825            }
826            true
827        }
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::super::testing::build;
834    use super::super::{OutlineContext, OutlinePlan, OutlineTables};
835    use super::*;
836    use crate::tables::loca::LocaGlyph;
837    use crate::{tables::glyf::Glyph, types::GlyphId, FontRef};
838    use alloc::{vec, vec::Vec};
839
840    /// At no size [`Scale26Dot6`] returns font units shifted left by six.
841    #[test]
842    fn unscaled_simple_glyph_is_font_units() {
843        let scale = Scale26Dot6::new(None, 1000);
844        let built = build(font_test_data::GLYF_COMPONENTS, 1, &scale);
845        let (points, contours) = (built.points, built.contours);
846        assert_eq!(contours, vec![3]);
847        assert_eq!(points.len(), 4);
848        for point in &points {
849            assert_eq!(point.x.to_bits() & 0x3F, 0, "expected whole font units");
850        }
851    }
852
853    /// True where the test font's glyph is a composite.
854    fn is_composite(gid: u32) -> bool {
855        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
856        let context = OutlineTables::new(&font).unwrap();
857        matches!(
858            context.glyph(GlyphId::new(gid)).unwrap().into_glyph(),
859            Some(Glyph::Composite(_))
860        )
861    }
862
863    /// At no size the two modes agree exactly on a simple glyph, and within a
864    /// font unit on a composite: 26.6 rounds a component's offset where
865    /// floating point does not, which is FreeType and HarfBuzz respectively.
866    #[test]
867    fn modes_agree_without_a_size() {
868        for gid in [0, 1, 5, 7] {
869            let slack = i32::from(is_composite(gid));
870            let fixed = build(
871                font_test_data::GLYF_COMPONENTS,
872                gid,
873                &Scale26Dot6::new(None, 1000),
874            );
875            let float = build(
876                font_test_data::GLYF_COMPONENTS,
877                gid,
878                &ScaleF32::new(None, 1000),
879            );
880            assert_eq!(fixed.contours, float.contours, "gid {gid}");
881            assert_eq!(fixed.points.len(), float.points.len(), "gid {gid}");
882            for (a, b) in fixed.points.iter().zip(&float.points) {
883                assert!(
884                    ((a.x.to_bits() >> 6) - b.x as i32).abs() <= slack,
885                    "gid {gid}"
886                );
887                assert!(
888                    ((a.y.to_bits() >> 6) - b.y as i32).abs() <= slack,
889                    "gid {gid}"
890                );
891            }
892        }
893    }
894
895    /// Phantom points are scaled by their own trait methods, which the
896    /// contour points never reach.
897    #[test]
898    fn modes_agree_on_phantom_points() {
899        for gid in [0, 1, 5, 7] {
900            let fixed = build(
901                font_test_data::GLYF_COMPONENTS,
902                gid,
903                &Scale26Dot6::new(None, 1000),
904            );
905            let float = build(
906                font_test_data::GLYF_COMPONENTS,
907                gid,
908                &ScaleF32::new(None, 1000),
909            );
910            for (a, b) in fixed.phantom.iter().zip(&float.phantom) {
911                assert_eq!(a.x.to_bits() >> 6, b.x as i32, "gid {gid}");
912                assert_eq!(a.y.to_bits() >> 6, b.y as i32, "gid {gid}");
913            }
914        }
915    }
916
917    /// Every input to `component_offset` is bounded, so the arithmetic cannot
918    /// overflow whatever the font says.
919    ///
920    /// An anchor offset is an `i16`. `FixedTransform` widens an `F2Dot14` to
921    /// at most two, so `hypot_fixed` of a pair is at most 2.75. `to_i32` is a
922    /// shift by sixteen, so a delta contributes at most 2^15. The sum stays
923    /// near 2^17; the measured worst case is 122,880, which `i32` clears by a
924    /// factor of seventeen thousand. A debug build panics
925    /// on overflow, so running this is most of the check; the bound is
926    /// asserted so a later change that widens an input is caught here.
927    #[test]
928    fn component_offset_stays_far_inside_i32() {
929        const BOUND: i32 = 1 << 18;
930        let extremes = [i16::MIN, i16::MAX];
931        let mut worst = 0i32;
932        for x in extremes {
933            for y in extremes {
934                for basis in extremes {
935                    let transform = Transform {
936                        xx: F2Dot14::from_bits(basis),
937                        yx: F2Dot14::from_bits(basis),
938                        xy: F2Dot14::from_bits(basis),
939                        yy: F2Dot14::from_bits(basis),
940                    };
941                    let offset = Point::new(x as i32, y as i32);
942                    let delta = Point::new(Fixed::from_bits(i32::MAX), Fixed::from_bits(i32::MIN));
943                    for scale_offset in [false, true] {
944                        let raw = Unscaled.component_offset(
945                            offset,
946                            &transform,
947                            scale_offset,
948                            Some(delta),
949                            OffsetRounding::default(),
950                        );
951                        worst = worst.max(raw.x.abs()).max(raw.y.abs());
952                        // The scaled modes run the same sum before scaling it.
953                        let fixed = Scale26Dot6::new(Some(2048.0), 16).component_offset(
954                            offset,
955                            &transform,
956                            scale_offset,
957                            Some(delta),
958                            OffsetRounding { x: true, y: true },
959                        );
960                        assert!(fixed.x.to_bits().abs() < i32::MAX / 2);
961                        assert!(fixed.y.to_bits().abs() < i32::MAX / 2);
962                        let float = ScaleF32::new(Some(2048.0), 16).component_offset(
963                            offset,
964                            &transform,
965                            scale_offset,
966                            Some(Point::new(f32::from(i16::MAX), f32::from(i16::MIN))),
967                            OffsetRounding::default(),
968                        );
969                        assert!(float.x.is_finite() && float.y.is_finite());
970                    }
971                }
972            }
973        }
974        assert!(worst < BOUND, "worst offset was {worst}");
975    }
976
977    /// Whether a size was asked for decides whether [`OutlinePlan::load`](super::super::OutlinePlan::load)
978    /// hints, so this is checked directly rather than through its effect.
979    #[test]
980    fn a_scale_knows_whether_it_has_a_size() {
981        assert!(!Scale26Dot6::new(None, 1000).is_scaled());
982        assert!(Scale26Dot6::new(Some(16.0), 1000).is_scaled());
983        assert!(!ScaleF32::new(None, 1000).is_scaled());
984        assert!(ScaleF32::new(Some(16.0), 1000).is_scaled());
985        assert!(!Unscaled.is_scaled());
986    }
987
988    /// [`Unscaled`] is measured against `glyf` itself rather than another
989    /// scale, so a change that moved both is still caught.
990    #[test]
991    fn unscaled_output_is_the_glyf_coordinates() {
992        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
993        let context = OutlineTables::new(&font).unwrap();
994        let Some(LocaGlyph::Glyph(Glyph::Simple(simple))) = context.glyph(GlyphId::new(1)) else {
995            panic!("glyph 1 is simple");
996        };
997        let raw: Vec<_> = simple.points().map(|point| (point.x, point.y)).collect();
998        let built = build(font_test_data::GLYF_COMPONENTS, 1, &Unscaled);
999        assert_eq!(built.points.len(), raw.len());
1000        for (point, (x, y)) in built.points.iter().zip(&raw) {
1001            assert_eq!((point.x, point.y), (i32::from(*x), i32::from(*y)));
1002        }
1003    }
1004
1005    /// [`Unscaled`] needs no separate font unit buffer, since its output
1006    /// already is one.
1007    #[test]
1008    fn unscaled_needs_no_second_buffer() {
1009        const { assert!(!Unscaled::NEEDS_UNSCALED) };
1010        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1011        let context = OutlineTables::new(&font).unwrap();
1012        let plan = OutlinePlan::new(&context, GlyphId::new(1)).unwrap();
1013        assert_eq!(plan.buffer_lengths::<Unscaled>().unscaled, 0);
1014    }
1015
1016    /// Scaling halves at half the size, for a composite as well as a simple
1017    /// glyph: a component's offset is scaled before it reaches points that
1018    /// are already scaled.
1019    #[test]
1020    fn scaling_is_proportional() {
1021        for gid in [1, 5] {
1022            let small = build(
1023                font_test_data::GLYF_COMPONENTS,
1024                gid,
1025                &Scale26Dot6::new(Some(16.0), 1000),
1026            )
1027            .points;
1028            let large = build(
1029                font_test_data::GLYF_COMPONENTS,
1030                gid,
1031                &Scale26Dot6::new(Some(32.0), 1000),
1032            )
1033            .points;
1034            // Within a 26.6 unit of exactly double, and one more for a
1035            // composite, whose offset is rounded once at each size.
1036            let slack = 1 + i32::from(is_composite(gid));
1037            for (s, l) in small.iter().zip(&large) {
1038                assert!(
1039                    (l.x.to_bits() - 2 * s.x.to_bits()).abs() <= slack,
1040                    "gid {gid}"
1041                );
1042                assert!(
1043                    (l.y.to_bits() - 2 * s.y.to_bits()).abs() <= slack,
1044                    "gid {gid}"
1045                );
1046            }
1047        }
1048    }
1049}