use core::ops::RangeInclusive; use raw::tables::glyf::PointCoord; use read_fonts::{ tables::glyf::{PointFlags, PointMarker}, tables::gvar::{GlyphDelta, Gvar}, tables::variations::TupleVariation, types::{F2Dot14, Fixed, GlyphId, Point}, ReadError, }; use super::PHANTOM_POINT_COUNT; /// Compute a set of deltas for the component offsets of a composite glyph. /// /// Interpolation is meaningless for component offsets so this is a /// specialized function that skips the expensive bits. pub(super) fn composite_glyph( gvar: &Gvar, glyph_id: GlyphId, coords: &[F2Dot14], deltas: &mut [Point], ) -> Result<(), ReadError> { compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| { for tuple_delta in tuple.deltas() { let ix = tuple_delta.position as usize; if let Some(delta) = deltas.get_mut(ix) { *delta += tuple_delta.apply_scalar(scalar); } } Ok(()) })?; Ok(()) } pub(super) struct SimpleGlyph<'a, C: PointCoord> { pub points: &'a [Point], pub flags: &'a mut [PointFlags], pub contours: &'a [u16], } /// Compute a set of deltas for the points in a simple glyph. /// /// This function will use interpolation to infer missing deltas for tuples /// that contain sparse sets. The `iup_buffer` buffer is temporary storage /// used for this and the length must be >= glyph.points.len(). pub(super) fn simple_glyph( gvar: &Gvar, glyph_id: GlyphId, coords: &[F2Dot14], glyph: SimpleGlyph, iup_buffer: &mut [Point], deltas: &mut [Point], ) -> Result<(), ReadError> where C: PointCoord, D: PointCoord, D: From, { if iup_buffer.len() < glyph.points.len() || glyph.points.len() < PHANTOM_POINT_COUNT { return Err(ReadError::InvalidArrayLen); } if gvar.glyph_variation_data(glyph_id).is_err() { // Empty variation data for a glyph is not an error. return Ok(()); }; let SimpleGlyph { points, flags, contours, } = glyph; compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| { // Infer missing deltas by interpolation. // Prepare our working buffer by converting the points to 16.16 // and clearing the HAS_DELTA flags. for ((flag, point), iup_point) in flags.iter_mut().zip(points).zip(&mut iup_buffer[..]) { *iup_point = point.map(D::from); flag.clear_marker(PointMarker::HAS_DELTA); } tuple.accumulate_sparse_deltas(iup_buffer, flags, scalar)?; interpolate_deltas(points, flags, contours, &mut iup_buffer[..]) .ok_or(ReadError::OutOfBounds)?; for ((delta, point), iup_point) in deltas.iter_mut().zip(points).zip(iup_buffer.iter()) { *delta += *iup_point - point.map(D::from); } Ok(()) })?; Ok(()) } /// The common parts of simple and complex glyph processing fn compute_deltas_for_glyph( gvar: &Gvar, glyph_id: GlyphId, coords: &[F2Dot14], deltas: &mut [Point], mut apply_tuple_missing_deltas_fn: impl FnMut( Fixed, TupleVariation, &mut [Point], ) -> Result<(), ReadError>, ) -> Result<(), ReadError> where C: PointCoord, D: PointCoord, D: From, { for delta in deltas.iter_mut() { *delta = Default::default(); } let Ok(Some(var_data)) = gvar.glyph_variation_data(glyph_id) else { // Empty variation data for a glyph is not an error. return Ok(()); }; for (tuple, scalar) in var_data.active_tuples_at(coords) { // Fast path: tuple contains all points, we can simply accumulate // the deltas directly. if tuple.has_deltas_for_all_points() { tuple.accumulate_dense_deltas(deltas, scalar)?; } else { // Slow path is, annoyingly, different for simple vs composite // so let the caller handle it apply_tuple_missing_deltas_fn(scalar, tuple, deltas)?; } } Ok(()) } /// Interpolate points without delta values, similar to the IUP hinting /// instruction. /// /// Modeled after the FreeType implementation: /// fn interpolate_deltas( points: &[Point], flags: &[PointFlags], contours: &[u16], out_points: &mut [Point], ) -> Option<()> where C: PointCoord, D: PointCoord, D: From, { let mut jiggler = Jiggler { points, out_points }; let mut point_ix = 0usize; for &end_point_ix in contours { let end_point_ix = end_point_ix as usize; let first_point_ix = point_ix; // Search for first point that has a delta. while point_ix <= end_point_ix && !flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) { point_ix += 1; } // If we didn't find any deltas, no variations in the current tuple // apply, so skip it. if point_ix > end_point_ix { continue; } let first_delta_ix = point_ix; let mut cur_delta_ix = point_ix; point_ix += 1; // Search for next point that has a delta... while point_ix <= end_point_ix { if flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) { // ... and interpolate intermediate points. jiggler.interpolate( cur_delta_ix + 1..=point_ix - 1, RefPoints(cur_delta_ix, point_ix), )?; cur_delta_ix = point_ix; } point_ix += 1; } // If we only have a single delta, shift the contour. if cur_delta_ix == first_delta_ix { jiggler.shift(first_point_ix..=end_point_ix, cur_delta_ix)?; } else { // Otherwise, handle remaining points at beginning and end of // contour. jiggler.interpolate( cur_delta_ix + 1..=end_point_ix, RefPoints(cur_delta_ix, first_delta_ix), )?; if first_delta_ix > 0 { jiggler.interpolate( first_point_ix..=first_delta_ix - 1, RefPoints(cur_delta_ix, first_delta_ix), )?; } } } Some(()) } struct RefPoints(usize, usize); struct Jiggler<'a, C, D> where C: PointCoord, D: PointCoord, D: From, { points: &'a [Point], out_points: &'a mut [Point], } impl Jiggler<'_, C, D> where C: PointCoord, D: PointCoord, D: From, { /// Shift the coordinates of all points in the specified range using the /// difference given by the point at `ref_ix`. /// /// Modeled after the FreeType implementation: fn shift(&mut self, range: RangeInclusive, ref_ix: usize) -> Option<()> { let ref_in = self.points.get(ref_ix)?.map(D::from); let ref_out = self.out_points.get(ref_ix)?; let delta = *ref_out - ref_in; if delta.x == D::zeroed() && delta.y == D::zeroed() { return Some(()); } // Apply the reference point delta to the entire range excluding the // reference point itself which would apply the delta twice. for out_point in self.out_points.get_mut(*range.start()..ref_ix)? { *out_point += delta; } for out_point in self.out_points.get_mut(ref_ix + 1..=*range.end())? { *out_point += delta; } Some(()) } /// Interpolate the coordinates of all points in the specified range using /// `ref1_ix` and `ref2_ix` as the reference point indices. /// /// Modeled after the FreeType implementation: /// /// For details on the algorithm, see: fn interpolate(&mut self, range: RangeInclusive, ref_points: RefPoints) -> Option<()> { if range.is_empty() { return Some(()); } // FreeType uses pointer tricks to handle x and y coords with a single piece of code. // Try a macro instead. macro_rules! interp_coord { ($coord:ident) => { let RefPoints(mut ref1_ix, mut ref2_ix) = ref_points; if self.points.get(ref1_ix)?.$coord > self.points.get(ref2_ix)?.$coord { core::mem::swap(&mut ref1_ix, &mut ref2_ix); } let in1 = D::from(self.points.get(ref1_ix)?.$coord); let in2 = D::from(self.points.get(ref2_ix)?.$coord); let out1 = self.out_points.get(ref1_ix)?.$coord; let out2 = self.out_points.get(ref2_ix)?.$coord; // If the reference points have the same coordinate but different delta, // inferred delta is zero. Otherwise interpolate. if in1 != in2 || out1 == out2 { let scale = if in1 != in2 { (out2 - out1) / (in2 - in1) } else { D::zeroed() }; let d1 = out1 - in1; let d2 = out2 - in2; for (point, out_point) in self .points .get(range.clone())? .iter() .zip(self.out_points.get_mut(range.clone())?) { let mut out = D::from(point.$coord); if out <= in1 { out += d1; } else if out >= in2 { out += d2; } else { out = out1 + (out - in1) * scale; } out_point.$coord = out; } } }; } interp_coord!(x); interp_coord!(y); Some(()) } } #[cfg(test)] mod tests { use super::*; fn make_points(tuples: &[(i32, i32)]) -> Vec> { tuples.iter().map(|&(x, y)| Point::new(x, y)).collect() } fn make_working_points_and_flags( points: &[Point], deltas: &[Point], ) -> (Vec>, Vec) { let working_points = points .iter() .zip(deltas) .map(|(point, delta)| point.map(Fixed::from_i32) + delta.map(Fixed::from_i32)) .collect(); let flags = deltas .iter() .map(|delta| { let mut flags = PointFlags::default(); if delta.x != 0 || delta.y != 0 { flags.set_marker(PointMarker::HAS_DELTA); } flags }) .collect(); (working_points, flags) } #[test] fn shift() { let points = make_points(&[(245, 630), (260, 700), (305, 680)]); // Single delta triggers a full contour shift. let deltas = make_points(&[(20, -10), (0, 0), (0, 0)]); let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas); interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap(); let expected = &[ Point::new(265, 620).map(Fixed::from_i32), Point::new(280, 690).map(Fixed::from_i32), Point::new(325, 670).map(Fixed::from_i32), ]; assert_eq!(&working_points, expected); } #[test] fn interpolate() { // Test taken from the spec: // https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers // with a minor adjustment to account for the precision of our fixed point math. let points = make_points(&[(245, 630), (260, 700), (305, 680)]); let deltas = make_points(&[(28, -62), (0, 0), (-42, -57)]); let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas); interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap(); assert_eq!( working_points[1], Point::new( Fixed::from_f64(260.0 + 10.4999237060547), Fixed::from_f64(700.0 - 57.0) ) ); } }