skrifa/outline/glyf/
deltas.rs

1use core::ops::RangeInclusive;
2
3use raw::tables::glyf::PointCoord;
4use read_fonts::{
5    tables::glyf::{PointFlags, PointMarker},
6    tables::gvar::{GlyphDelta, Gvar},
7    tables::variations::TupleVariation,
8    types::{F2Dot14, Fixed, GlyphId, Point},
9    ReadError,
10};
11
12use super::PHANTOM_POINT_COUNT;
13
14/// Compute a set of deltas for the component offsets of a composite glyph.
15///
16/// Interpolation is meaningless for component offsets so this is a
17/// specialized function that skips the expensive bits.
18pub(super) fn composite_glyph<D: PointCoord>(
19    gvar: &Gvar,
20    glyph_id: GlyphId,
21    coords: &[F2Dot14],
22    deltas: &mut [Point<D>],
23) -> Result<(), ReadError> {
24    compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| {
25        for tuple_delta in tuple.deltas() {
26            let ix = tuple_delta.position as usize;
27            if let Some(delta) = deltas.get_mut(ix) {
28                *delta += tuple_delta.apply_scalar(scalar);
29            }
30        }
31        Ok(())
32    })?;
33    Ok(())
34}
35
36pub(super) struct SimpleGlyph<'a, C: PointCoord> {
37    pub points: &'a [Point<C>],
38    pub flags: &'a mut [PointFlags],
39    pub contours: &'a [u16],
40}
41
42/// Compute a set of deltas for the points in a simple glyph.
43///
44/// This function will use interpolation to infer missing deltas for tuples
45/// that contain sparse sets. The `iup_buffer` buffer is temporary storage
46/// used for this and the length must be >= glyph.points.len().
47pub(super) fn simple_glyph<C, D>(
48    gvar: &Gvar,
49    glyph_id: GlyphId,
50    coords: &[F2Dot14],
51    glyph: SimpleGlyph<C>,
52    iup_buffer: &mut [Point<D>],
53    deltas: &mut [Point<D>],
54) -> Result<(), ReadError>
55where
56    C: PointCoord,
57    D: PointCoord,
58    D: From<C>,
59{
60    if iup_buffer.len() < glyph.points.len() || glyph.points.len() < PHANTOM_POINT_COUNT {
61        return Err(ReadError::InvalidArrayLen);
62    }
63    for delta in deltas.iter_mut() {
64        *delta = Default::default();
65    }
66    if gvar.glyph_variation_data(glyph_id).is_err() {
67        // Empty variation data for a glyph is not an error.
68        return Ok(());
69    };
70    let SimpleGlyph {
71        points,
72        flags,
73        contours,
74    } = glyph;
75    compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| {
76        // Infer missing deltas by interpolation.
77        // Prepare our working buffer by converting the points to 16.16
78        // and clearing the HAS_DELTA flags.
79        for ((flag, point), iup_point) in flags.iter_mut().zip(points).zip(&mut iup_buffer[..]) {
80            *iup_point = point.map(D::from);
81            flag.clear_marker(PointMarker::HAS_DELTA);
82        }
83        tuple.accumulate_sparse_deltas(iup_buffer, flags, scalar)?;
84        interpolate_deltas(points, flags, contours, &mut iup_buffer[..])
85            .ok_or(ReadError::OutOfBounds)?;
86        for ((delta, point), iup_point) in deltas.iter_mut().zip(points).zip(iup_buffer.iter()) {
87            *delta += *iup_point - point.map(D::from);
88        }
89        Ok(())
90    })?;
91    Ok(())
92}
93
94/// The common parts of simple and complex glyph processing
95fn compute_deltas_for_glyph<C, D>(
96    gvar: &Gvar,
97    glyph_id: GlyphId,
98    coords: &[F2Dot14],
99    deltas: &mut [Point<D>],
100    mut apply_tuple_missing_deltas_fn: impl FnMut(
101        Fixed,
102        TupleVariation<GlyphDelta>,
103        &mut [Point<D>],
104    ) -> Result<(), ReadError>,
105) -> Result<(), ReadError>
106where
107    C: PointCoord,
108    D: PointCoord,
109    D: From<C>,
110{
111    for delta in deltas.iter_mut() {
112        *delta = Default::default();
113    }
114    let Ok(Some(var_data)) = gvar.glyph_variation_data(glyph_id) else {
115        // Empty variation data for a glyph is not an error.
116        return Ok(());
117    };
118    for (tuple, scalar) in var_data.active_tuples_at(coords) {
119        // Fast path: tuple contains all points, we can simply accumulate
120        // the deltas directly.
121        if tuple.has_deltas_for_all_points() {
122            tuple.accumulate_dense_deltas(deltas, scalar)?;
123        } else {
124            // Slow path is, annoyingly, different for simple vs composite
125            // so let the caller handle it
126            apply_tuple_missing_deltas_fn(scalar, tuple, deltas)?;
127        }
128    }
129    Ok(())
130}
131
132/// Interpolate points without delta values, similar to the IUP hinting
133/// instruction.
134///
135/// Modeled after the FreeType implementation:
136/// <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3881>
137fn interpolate_deltas<C, D>(
138    points: &[Point<C>],
139    flags: &[PointFlags],
140    contours: &[u16],
141    out_points: &mut [Point<D>],
142) -> Option<()>
143where
144    C: PointCoord,
145    D: PointCoord,
146    D: From<C>,
147{
148    let mut jiggler = Jiggler { points, out_points };
149    let mut point_ix = 0usize;
150    for &end_point_ix in contours {
151        let end_point_ix = end_point_ix as usize;
152        let first_point_ix = point_ix;
153        // Search for first point that has a delta.
154        while point_ix <= end_point_ix && !flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) {
155            point_ix += 1;
156        }
157        // If we didn't find any deltas, no variations in the current tuple
158        // apply, so skip it.
159        if point_ix > end_point_ix {
160            continue;
161        }
162        let first_delta_ix = point_ix;
163        let mut cur_delta_ix = point_ix;
164        point_ix += 1;
165        // Search for next point that has a delta...
166        while point_ix <= end_point_ix {
167            if flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) {
168                // ... and interpolate intermediate points.
169                jiggler.interpolate(
170                    cur_delta_ix + 1..=point_ix - 1,
171                    RefPoints(cur_delta_ix, point_ix),
172                )?;
173                cur_delta_ix = point_ix;
174            }
175            point_ix += 1;
176        }
177        // If we only have a single delta, shift the contour.
178        if cur_delta_ix == first_delta_ix {
179            jiggler.shift(first_point_ix..=end_point_ix, cur_delta_ix)?;
180        } else {
181            // Otherwise, handle remaining points at beginning and end of
182            // contour.
183            jiggler.interpolate(
184                cur_delta_ix + 1..=end_point_ix,
185                RefPoints(cur_delta_ix, first_delta_ix),
186            )?;
187            if first_delta_ix > 0 {
188                jiggler.interpolate(
189                    first_point_ix..=first_delta_ix - 1,
190                    RefPoints(cur_delta_ix, first_delta_ix),
191                )?;
192            }
193        }
194    }
195    Some(())
196}
197
198struct RefPoints(usize, usize);
199
200struct Jiggler<'a, C, D>
201where
202    C: PointCoord,
203    D: PointCoord,
204    D: From<C>,
205{
206    points: &'a [Point<C>],
207    out_points: &'a mut [Point<D>],
208}
209
210impl<C, D> Jiggler<'_, C, D>
211where
212    C: PointCoord,
213    D: PointCoord,
214    D: From<C>,
215{
216    /// Shift the coordinates of all points in the specified range using the
217    /// difference given by the point at `ref_ix`.
218    ///
219    /// Modeled after the FreeType implementation: <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3776>
220    fn shift(&mut self, range: RangeInclusive<usize>, ref_ix: usize) -> Option<()> {
221        let ref_in = self.points.get(ref_ix)?.map(D::from);
222        let ref_out = self.out_points.get(ref_ix)?;
223        let delta = *ref_out - ref_in;
224        if delta.x == D::zeroed() && delta.y == D::zeroed() {
225            return Some(());
226        }
227        // Apply the reference point delta to the entire range excluding the
228        // reference point itself which would apply the delta twice.
229        for out_point in self.out_points.get_mut(*range.start()..ref_ix)? {
230            *out_point += delta;
231        }
232        for out_point in self.out_points.get_mut(ref_ix + 1..=*range.end())? {
233            *out_point += delta;
234        }
235        Some(())
236    }
237
238    /// Interpolate the coordinates of all points in the specified range using
239    /// `ref1_ix` and `ref2_ix` as the reference point indices.
240    ///
241    /// Modeled after the FreeType implementation: <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3813>
242    ///
243    /// For details on the algorithm, see: <https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers>
244    fn interpolate(&mut self, range: RangeInclusive<usize>, ref_points: RefPoints) -> Option<()> {
245        if range.is_empty() {
246            return Some(());
247        }
248        // FreeType uses pointer tricks to handle x and y coords with a single piece of code.
249        // Try a macro instead.
250        macro_rules! interp_coord {
251            ($coord:ident) => {
252                let RefPoints(mut ref1_ix, mut ref2_ix) = ref_points;
253                if self.points.get(ref1_ix)?.$coord > self.points.get(ref2_ix)?.$coord {
254                    core::mem::swap(&mut ref1_ix, &mut ref2_ix);
255                }
256                let in1 = D::from(self.points.get(ref1_ix)?.$coord);
257                let in2 = D::from(self.points.get(ref2_ix)?.$coord);
258                let out1 = self.out_points.get(ref1_ix)?.$coord;
259                let out2 = self.out_points.get(ref2_ix)?.$coord;
260                // If the reference points have the same coordinate but different delta,
261                // inferred delta is zero. Otherwise interpolate.
262                if in1 != in2 || out1 == out2 {
263                    let scale = if in1 != in2 {
264                        (out2 - out1) / (in2 - in1)
265                    } else {
266                        D::zeroed()
267                    };
268                    let d1 = out1 - in1;
269                    let d2 = out2 - in2;
270                    for (point, out_point) in self
271                        .points
272                        .get(range.clone())?
273                        .iter()
274                        .zip(self.out_points.get_mut(range.clone())?)
275                    {
276                        let mut out = D::from(point.$coord);
277                        if out <= in1 {
278                            out += d1;
279                        } else if out >= in2 {
280                            out += d2;
281                        } else {
282                            out = out1 + (out - in1) * scale;
283                        }
284                        out_point.$coord = out;
285                    }
286                }
287            };
288        }
289        interp_coord!(x);
290        interp_coord!(y);
291        Some(())
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    fn make_points(tuples: &[(i32, i32)]) -> Vec<Point<i32>> {
300        tuples.iter().map(|&(x, y)| Point::new(x, y)).collect()
301    }
302
303    fn make_working_points_and_flags(
304        points: &[Point<i32>],
305        deltas: &[Point<i32>],
306    ) -> (Vec<Point<Fixed>>, Vec<PointFlags>) {
307        let working_points = points
308            .iter()
309            .zip(deltas)
310            .map(|(point, delta)| point.map(Fixed::from_i32) + delta.map(Fixed::from_i32))
311            .collect();
312        let flags = deltas
313            .iter()
314            .map(|delta| {
315                let mut flags = PointFlags::default();
316                if delta.x != 0 || delta.y != 0 {
317                    flags.set_marker(PointMarker::HAS_DELTA);
318                }
319                flags
320            })
321            .collect();
322        (working_points, flags)
323    }
324
325    #[test]
326    fn shift() {
327        let points = make_points(&[(245, 630), (260, 700), (305, 680)]);
328        // Single delta triggers a full contour shift.
329        let deltas = make_points(&[(20, -10), (0, 0), (0, 0)]);
330        let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas);
331        interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap();
332        let expected = &[
333            Point::new(265, 620).map(Fixed::from_i32),
334            Point::new(280, 690).map(Fixed::from_i32),
335            Point::new(325, 670).map(Fixed::from_i32),
336        ];
337        assert_eq!(&working_points, expected);
338    }
339
340    #[test]
341    fn interpolate() {
342        // Test taken from the spec:
343        // https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers
344        // with a minor adjustment to account for the precision of our fixed point math.
345        let points = make_points(&[(245, 630), (260, 700), (305, 680)]);
346        let deltas = make_points(&[(28, -62), (0, 0), (-42, -57)]);
347        let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas);
348        interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap();
349        assert_eq!(
350            working_points[1],
351            Point::new(
352                Fixed::from_f64(260.0 + 10.4999237060547),
353                Fixed::from_f64(700.0 - 57.0)
354            )
355        );
356    }
357}