Skip to main content

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