//! Support for rendering variable composite glyphs from the VARC table. use read_fonts::{ tables::{ layout::Condition, varc::{ DecomposedTransform, MultiItemVariationStore, SparseVariationRegionList, Varc, VarcComponent, VarcFlags, }, variations::NO_VARIATION_INDEX, }, types::{F2Dot14, GlyphId, Matrix}, FontRef, ReadError, TableProvider, }; use crate::{ collections::SmallVec, instance::Size, outline::{cff, glyf, metrics::GlyphHMetrics, pen::PathStyle, DrawError, OutlinePen}, provider::MetadataProvider, GLYF_COMPOSITE_RECURSION_LIMIT, }; #[cfg(feature = "libm")] #[allow(unused_imports)] use core_maths::CoreFloat; use super::OutlineKind; type GlyphStack = SmallVec; type CoordVec = SmallVec; type AxisIndexVec = SmallVec; type AxisValueVec = SmallVec; type DeltaVec = SmallVec; type ScalarCacheVec = SmallVec; type Affine = Matrix; struct Scratchpad { deltas: DeltaVec, axis_indices: AxisIndexVec, axis_values: AxisValueVec, } impl Scratchpad { fn new() -> Self { Self { deltas: DeltaVec::new(), axis_indices: AxisIndexVec::new(), axis_values: AxisValueVec::new(), } } } struct VarcSharedContext<'a, 'b> { font_coords: &'b [F2Dot14], size: Size, path_style: PathStyle, coverage: &'b read_fonts::tables::layout::CoverageTable<'a>, var_store: Option<&'b MultiItemVariationStore<'a>>, store_regions: Option<( &'b MultiItemVariationStore<'a>, &'b SparseVariationRegionList<'a>, )>, } #[derive(Clone)] enum BaseOutlines<'a> { Glyf(glyf::Outlines<'a>), Cff(cff::Outlines<'a>), } impl<'a> BaseOutlines<'a> { fn glyph_count(&self) -> u32 { match self { Self::Glyf(glyf) => glyf.glyph_count() as u32, Self::Cff(cff) => cff.glyph_count() as u32, } } fn font(&self) -> &FontRef<'a> { match self { Self::Glyf(glyf) => &glyf.font, Self::Cff(cff) => &cff.font, } } fn base_outline_kind(&self, glyph_id: GlyphId) -> Option> { match self { Self::Glyf(glyf) => Some(OutlineKind::Glyf( glyf.clone(), glyf.outline(glyph_id).ok()?, )), Self::Cff(cff) => Some(OutlineKind::Cff( cff.clone(), glyph_id, cff.subfont_index(glyph_id), )), } } fn base_outline_memory(&self, glyph_id: GlyphId) -> usize { match self { Self::Glyf(glyf) => glyf .outline(glyph_id) .ok() .map(|outline| outline.required_buffer_size(super::Hinting::None)) .unwrap_or(0), Self::Cff(..) => 0, } } } #[derive(Clone)] pub(crate) struct Outlines<'a> { varc: Varc<'a>, coverage: read_fonts::tables::layout::CoverageTable<'a>, var_store: Option>, regions: Option>, base: BaseOutlines<'a>, glyph_metrics: GlyphHMetrics<'a>, units_per_em: u16, axis_count: usize, } #[derive(Clone, Copy)] pub(crate) struct Outline { pub(crate) glyph_id: GlyphId, pub(crate) coverage_index: u16, max_component_memory: usize, } impl Outline { pub fn required_buffer_size(&self) -> usize { self.max_component_memory } } impl<'a> Outlines<'a> { pub fn new(font: &FontRef<'a>) -> Option { let varc = font.varc().ok()?; if let Some(glyf) = glyf::Outlines::new(font) { return Self::from_base(font, varc, BaseOutlines::Glyf(glyf)); } if let Some(cff) = cff::Outlines::new(font) { return Self::from_base(font, varc, BaseOutlines::Cff(cff)); } None } fn from_base(font: &FontRef<'a>, varc: Varc<'a>, base: BaseOutlines<'a>) -> Option { let glyph_metrics = GlyphHMetrics::new(font)?; let units_per_em = font.head().ok()?.units_per_em(); let axis_count = font.axes().len(); let coverage = varc.coverage().ok()?; let var_store = varc.multi_var_store().transpose().ok()?; let regions = var_store .as_ref() .map(|s| s.region_list()) .transpose() .ok()?; Some(Self { varc, coverage, var_store, regions, base, glyph_metrics, units_per_em, axis_count, }) } pub fn units_per_em(&self) -> u16 { self.units_per_em } pub fn glyph_count(&self) -> u32 { self.base.glyph_count() } pub fn prefer_interpreter(&self) -> bool { false } pub fn fractional_size_hinting(&self) -> bool { false } pub fn font(&self) -> &FontRef<'a> { self.base.font() } pub(crate) fn fallback_outline_kind(&self, glyph_id: GlyphId) -> Option> { self.base.base_outline_kind(glyph_id) } pub fn outline(&self, glyph_id: GlyphId) -> Result, ReadError> { let Some(coverage_index) = self.coverage.get(glyph_id) else { return Ok(None); }; let max_component_memory = self.compute_max_component_memory(glyph_id, coverage_index)?; Ok(Some(Outline { glyph_id, coverage_index, max_component_memory, })) } /// Lightweight coverage lookup without computing max_component_memory. fn coverage_index(&self, glyph_id: GlyphId) -> Result, ReadError> { Ok(self.coverage.get(glyph_id)) } fn compute_max_component_memory( &self, glyph_id: GlyphId, coverage_index: u16, ) -> Result { let mut stack = GlyphStack::new(); self.max_component_memory_for_glyph(glyph_id, coverage_index, &mut stack) } fn max_component_memory_for_glyph( &self, glyph_id: GlyphId, coverage_index: u16, stack: &mut GlyphStack, ) -> Result { if stack.contains(&glyph_id) { return Ok(0); } if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT { return Ok(0); } stack.push(glyph_id); let mut max_memory = 0usize; let glyph = self.varc.glyph(coverage_index as usize)?; for component in glyph.components() { let component = component?; let component_gid = component.gid(); let component_memory = if component_gid == glyph_id { self.base.base_outline_memory(component_gid) } else if let Some(coverage_index) = self.coverage_index(component_gid)? { self.max_component_memory_for_glyph(component_gid, coverage_index, stack)? } else { self.base.base_outline_memory(component_gid) }; max_memory = max_memory.max(component_memory); } stack.pop(); Ok(max_memory) } pub fn draw( &self, outline: &Outline, buf: &mut [u8], size: Size, coords: &[F2Dot14], path_style: PathStyle, pen: &mut impl OutlinePen, ) -> Result<(), DrawError> { let mut font_coords = CoordVec::new(); expand_coords(&mut font_coords, self.axis_count, coords); let mut stack = GlyphStack::new(); let pen: &mut dyn OutlinePen = pen; let mut scalar_cache = self .scalar_cache_from_store(self.var_store.as_ref())? .unwrap(); let mut scratch = Scratchpad::new(); let ctx = VarcSharedContext { font_coords: &font_coords, size, path_style, coverage: &self.coverage, var_store: self.var_store.as_ref(), store_regions: self.var_store.as_ref().zip(self.regions.as_ref()), }; self.draw_glyph( outline.glyph_id, outline.coverage_index, &font_coords, Affine::IDENTITY, &ctx, buf, pen, &mut stack, &mut scalar_cache, &mut scratch, ) } pub fn draw_unscaled( &self, outline: &Outline, buf: &mut [u8], coords: &[F2Dot14], pen: &mut impl OutlinePen, ) -> Result { let size = Size::unscaled(); self.draw(outline, buf, size, coords, PathStyle::default(), pen)?; Ok(self.glyph_metrics.advance_width(outline.glyph_id, coords)) } #[allow(clippy::too_many_arguments)] fn draw_glyph( &self, glyph_id: GlyphId, coverage_index: u16, current_coords: &[F2Dot14], parent_matrix: Affine, ctx: &VarcSharedContext<'a, '_>, buf: &mut [u8], pen: &mut dyn OutlinePen, stack: &mut GlyphStack, scalar_cache: &mut ScalarCache, scratch: &mut Scratchpad, ) -> Result<(), DrawError> { if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT { return Err(DrawError::RecursionLimitExceeded(glyph_id)); } let glyph = self.varc.glyph(coverage_index as usize)?; stack.push(glyph_id); let coverage = ctx.coverage; let store_regions = ctx.store_regions; let mut component_coords_buffer = CoordVec::new(); let mut child_scalar_cache: Option = None; for component in glyph.components() { let component = component?; if !self.component_condition_met( &component, current_coords, scalar_cache, scratch, store_regions, )? { continue; } let component_gid = component.gid(); let flags = component.flags(); let coords_the_same = !flags.contains(VarcFlags::HAVE_AXES) && !flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES); let component_coords = if coords_the_same { current_coords } else { self.component_coords( &component, current_coords, &mut component_coords_buffer, scalar_cache, scratch, ctx.font_coords, store_regions, )?; component_coords_buffer.as_slice() }; let mut transform = *component.transform(); self.apply_transform_variations( &component, current_coords, &mut transform, scalar_cache, scratch, store_regions, )?; let scale = ctx.size.linear_scale(self.units_per_em); let matrix = parent_matrix * scale_matrix(transform.matrix(), scale); if component_gid != glyph_id { if let Some(coverage_index) = coverage.get(component_gid) { if !stack.contains(&component_gid) { // Optimization: if coordinates haven't changed, we can reuse the scalar cache. if coords_the_same { self.draw_glyph( component_gid, coverage_index, current_coords, matrix, ctx, buf, pen, stack, scalar_cache, scratch, )?; } else { if let Some(ref mut cache) = child_scalar_cache { cache.values.fill(ScalarCache::INVALID); } else { child_scalar_cache = self.scalar_cache_from_store(ctx.var_store)?; } self.draw_glyph( component_gid, coverage_index, component_coords, matrix, ctx, buf, pen, stack, child_scalar_cache.as_mut().unwrap(), scratch, )?; } continue; } } } let mut transform_pen = TransformPen::new(pen, matrix); self.draw_base_glyph( component_gid, component_coords, ctx.size, ctx.path_style, buf, &mut transform_pen, )?; } stack.pop(); Ok(()) } fn draw_base_glyph( &self, glyph_id: GlyphId, coords: &[F2Dot14], size: Size, path_style: PathStyle, buf: &mut [u8], pen: &mut impl OutlinePen, ) -> Result<(), DrawError> { let Some(kind) = self.base.base_outline_kind(glyph_id) else { return Err(DrawError::GlyphNotFound(glyph_id)); }; let settings = crate::outline::DrawSettings::unhinted(size, crate::instance::LocationRef::new(coords)) .with_path_style(path_style) .with_memory(Some(buf)); crate::outline::OutlineGlyph { kind }.draw(settings, pen)?; Ok(()) } #[allow(clippy::too_many_arguments)] fn component_coords( &self, component: &VarcComponent<'_>, current_coords: &[F2Dot14], coords: &mut CoordVec, scalar_cache: &mut ScalarCache, scratch: &mut Scratchpad, font_coords: &[F2Dot14], store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>, ) -> Result<(), DrawError> { let flags = component.flags(); if flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES) { expand_coords(coords, font_coords.len(), font_coords); } else { expand_coords(coords, current_coords.len(), current_coords); } if !flags.contains(VarcFlags::HAVE_AXES) { return Ok(()); } let axis_indices_index = component .axis_indices_index() .ok_or(ReadError::MalformedData("Missing axisIndicesIndex"))?; let num_axes = self.axis_indices(axis_indices_index as usize, &mut scratch.axis_indices)?; self.axis_values(component, num_axes, &mut scratch.axis_values)?; if let Some(var_idx) = component.axis_values_var_index() { let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?; compute_tuple_deltas( store, regions, var_idx, current_coords, scratch.axis_indices.len(), scalar_cache, &mut scratch.deltas, )?; for (value, delta) in scratch.axis_values.iter_mut().zip(scratch.deltas.iter()) { *value += *delta; } } for (axis_index, value) in scratch .axis_indices .iter() .zip(scratch.axis_values.iter().copied()) { let Some(slot) = coords.get_mut(*axis_index as usize) else { return Err(DrawError::Read(ReadError::OutOfBounds)); }; let raw = value.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16; *slot = F2Dot14::from_bits(raw); } Ok(()) } fn axis_indices(&self, nth: usize, out: &mut AxisIndexVec) -> Result { let packed = self.varc.axis_indices(nth)?; out.clear(); for value in packed.iter() { out.push(value as u16); } Ok(out.len()) } fn axis_values( &self, component: &VarcComponent<'_>, count: usize, out: &mut AxisValueVec, ) -> Result<(), DrawError> { let Some(packed) = component.axis_values() else { out.clear(); return Ok(()); }; out.resize_and_fill(count, 0.0); for (slot, value) in out.iter_mut().zip(packed.iter().by_ref().take(count)) { *slot = value as f32; } Ok(()) } fn apply_transform_variations( &self, component: &VarcComponent<'_>, coords: &[F2Dot14], transform: &mut DecomposedTransform, scalar_cache: &mut ScalarCache, scratch: &mut Scratchpad, store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>, ) -> Result<(), DrawError> { let Some(var_idx) = component.transform_var_index() else { return Ok(()); }; let flags = component.flags(); // Count transform fields using a mask + count_ones const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate( VarcFlags::HAVE_TRANSLATE_X.bits() | VarcFlags::HAVE_TRANSLATE_Y.bits() | VarcFlags::HAVE_ROTATION.bits() | VarcFlags::HAVE_SCALE_X.bits() | VarcFlags::HAVE_SCALE_Y.bits() | VarcFlags::HAVE_SKEW_X.bits() | VarcFlags::HAVE_SKEW_Y.bits() | VarcFlags::HAVE_TCENTER_X.bits() | VarcFlags::HAVE_TCENTER_Y.bits(), ); let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize; if field_count == 0 { return Ok(()); } let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?; compute_tuple_deltas( store, regions, var_idx, coords, field_count, scalar_cache, &mut scratch.deltas, )?; // Apply deltas in flag order, consuming from iterator let mut delta_iter = scratch.deltas.iter().copied(); if flags.contains(VarcFlags::HAVE_TRANSLATE_X) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_translate_x(transform.translate_x() + delta); } if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_translate_y(transform.translate_y() + delta); } if flags.contains(VarcFlags::HAVE_ROTATION) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_rotation(transform.rotation() + delta / 4096.0); } if flags.contains(VarcFlags::HAVE_SCALE_X) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_scale_x(transform.scale_x() + delta / 1024.0); } if flags.contains(VarcFlags::HAVE_SCALE_Y) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_scale_y(transform.scale_y() + delta / 1024.0); } const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate( VarcFlags::HAVE_SKEW_X.bits() | VarcFlags::HAVE_SKEW_Y.bits() | VarcFlags::HAVE_TCENTER_X.bits() | VarcFlags::HAVE_TCENTER_Y.bits(), ); if flags.intersects(SKEW_OR_CENTER) { if flags.contains(VarcFlags::HAVE_SKEW_X) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_skew_x(transform.skew_x() + delta / 4096.0); } if flags.contains(VarcFlags::HAVE_SKEW_Y) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_skew_y(transform.skew_y() + delta / 4096.0); } if flags.contains(VarcFlags::HAVE_TCENTER_X) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_center_x(transform.center_x() + delta); } if flags.contains(VarcFlags::HAVE_TCENTER_Y) { let delta = delta_iter.next().unwrap_or(0.0); transform.set_center_y(transform.center_y() + delta); } } if !flags.contains(VarcFlags::HAVE_SCALE_Y) { transform.set_scale_y(transform.scale_x()); } Ok(()) } fn component_condition_met( &self, component: &VarcComponent<'_>, coords: &[F2Dot14], scalar_cache: &mut ScalarCache, scratch: &mut Scratchpad, store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>, ) -> Result { let Some(condition_index) = component.condition_index() else { return Ok(true); }; let Some(condition_list) = self.varc.condition_list() else { return Err(DrawError::Read(ReadError::NullOffset)); }; let condition_list = condition_list?; let condition = condition_list.conditions().get(condition_index as usize)?; let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?; Self::eval_condition(&condition, coords, store, regions, scalar_cache, scratch) } fn eval_condition( condition: &Condition<'a>, coords: &[F2Dot14], var_store: &MultiItemVariationStore<'a>, regions: &SparseVariationRegionList<'a>, scalar_cache: &mut ScalarCache, scratch: &mut Scratchpad, ) -> Result { match condition { Condition::Format1AxisRange(condition) => { let axis_index = condition.axis_index() as usize; let coord = coords.get(axis_index).copied().unwrap_or(F2Dot14::ZERO); Ok(coord >= condition.filter_range_min_value() && coord <= condition.filter_range_max_value()) } Condition::Format2VariableValue(condition) => { let default_value = condition.default_value() as f32; let var_idx = condition.var_index(); compute_tuple_deltas( var_store, regions, var_idx, coords, 1, scalar_cache, &mut scratch.deltas, )?; let delta = scratch.deltas.first().copied().unwrap_or(0.0); Ok(default_value + delta > 0.0) } Condition::Format3And(condition) => { for nested in condition.conditions().iter() { let nested = nested?; if !Self::eval_condition( &nested, coords, var_store, regions, scalar_cache, scratch, )? { return Ok(false); } } Ok(true) } Condition::Format4Or(condition) => { for nested in condition.conditions().iter() { let nested = nested?; if Self::eval_condition( &nested, coords, var_store, regions, scalar_cache, scratch, )? { return Ok(true); } } Ok(false) } Condition::Format5Negate(condition) => { let nested = condition.condition()?; Ok(!Self::eval_condition( &nested, coords, var_store, regions, scalar_cache, scratch, )?) } } } fn scalar_cache_from_store( &self, store: Option<&MultiItemVariationStore<'a>>, ) -> Result, DrawError> { let Some(store) = store else { return Ok(None); }; let region_count = store.region_list()?.region_count() as usize; Ok(Some(ScalarCache::new(region_count))) } } struct ScalarCache { values: ScalarCacheVec, } impl ScalarCache { const INVALID: f32 = 2.0; // Scalars are in [0,1], so 2.0 means "not cached" fn new(count: usize) -> Self { Self { values: ScalarCacheVec::with_len(count, Self::INVALID), } } fn get(&self, index: usize) -> f32 { self.values.get(index).copied().unwrap_or(Self::INVALID) } fn set(&mut self, index: usize, value: f32) { if let Some(slot) = self.values.get_mut(index) { *slot = value; } } } fn expand_coords(out: &mut CoordVec, axis_count: usize, coords: &[F2Dot14]) { out.resize_and_fill(axis_count, F2Dot14::ZERO); for (slot, value) in out.iter_mut().zip(coords.iter().copied()) { *slot = value; } } fn compute_tuple_deltas( store: &MultiItemVariationStore, regions: &SparseVariationRegionList, var_idx: u32, coords: &[F2Dot14], tuple_len: usize, cache: &mut ScalarCache, out: &mut DeltaVec, ) -> Result<(), ReadError> { out.resize_and_fill(tuple_len, 0.0); if tuple_len == 0 || var_idx == NO_VARIATION_INDEX { return Ok(()); } let outer = (var_idx >> 16) as usize; let inner = (var_idx & 0xFFFF) as usize; let data = store .variation_data() .get(outer) .map_err(|_| ReadError::InvalidCollectionIndex(outer as _))?; let region_indices = data.region_indices(); let mut deltas = data.delta_set(inner)?.fetcher(); let regions = regions.regions(); let out_slice = out.as_mut_slice(); let mut skip = 0; for region_index in region_indices.iter() { let region_idx = region_index.get() as usize; let mut scalar = cache.get(region_idx); if scalar >= 2.0 { scalar = regions.get(region_idx)?.compute_scalar_f32(coords); cache.set(region_idx, scalar); } // We skip lazily. Reduces work at the tail end. if scalar == 0.0 { skip += out_slice.len(); } else { if skip != 0 { deltas.skip(skip)?; skip = 0; } deltas.add_to_f32_scaled(out_slice, scalar)?; } } Ok(()) } #[inline(always)] fn scale_matrix(mut m: Affine, s: f32) -> Affine { m.dx *= s; m.dy *= s; m } struct TransformPen<'a, P: OutlinePen + ?Sized> { pen: &'a mut P, matrix: Affine, } impl<'a, P: OutlinePen + ?Sized> TransformPen<'a, P> { fn new(pen: &'a mut P, matrix: Affine) -> Self { Self { pen, matrix } } #[inline(always)] fn transform(&self, x: f32, y: f32) -> (f32, f32) { self.matrix.transform(x, y) } } impl OutlinePen for TransformPen<'_, P> { fn move_to(&mut self, x: f32, y: f32) { let (x, y) = self.transform(x, y); self.pen.move_to(x, y); } fn line_to(&mut self, x: f32, y: f32) { let (x, y) = self.transform(x, y); self.pen.line_to(x, y); } fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { let (cx0, cy0) = self.transform(cx0, cy0); let (x, y) = self.transform(x, y); self.pen.quad_to(cx0, cy0, x, y); } fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { let (cx0, cy0) = self.transform(cx0, cy0); let (cx1, cy1) = self.transform(cx1, cy1); let (x, y) = self.transform(x, y); self.pen.curve_to(cx0, cy0, cx1, cy1, x, y); } fn close(&mut self) { self.pen.close(); } } #[cfg(test)] mod tests { use super::*; use crate::outline::pen::PathElement; use read_fonts::{FontRef, TableProvider}; fn coord(value: f32) -> F2Dot14 { F2Dot14::from_f32(value) } fn assert_close(actual: f32, expected: f32) { let diff = (actual - expected).abs(); assert!( diff <= 1e-6, "expected {expected}, got {actual}, diff {diff}" ); } fn path_head_signature(path: &[PathElement], n: usize) -> Vec { path.iter() .take(n) .map(|el| match *el { PathElement::MoveTo { x, y } => format!("M{:.2},{:.2}", x, y), PathElement::LineTo { x, y } => format!("L{:.2},{:.2}", x, y), PathElement::QuadTo { cx0, cy0, x, y } => { format!("Q{:.2},{:.2} {:.2},{:.2}", cx0, cy0, x, y) } PathElement::CurveTo { cx0, cy0, cx1, cy1, x, y, } => format!( "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}", cx0, cy0, cx1, cy1, x, y ), PathElement::Close => "Z".to_string(), }) .collect() } #[test] fn expand_coords_pads_and_truncates() { let mut out = CoordVec::new(); expand_coords(&mut out, 4, &[coord(0.25), coord(-0.5)]); assert_eq!( out.as_slice(), &[coord(0.25), coord(-0.5), F2Dot14::ZERO, F2Dot14::ZERO] ); expand_coords(&mut out, 1, &[coord(0.25), coord(-0.5)]); assert_eq!(out.as_slice(), &[coord(0.25)]); } #[test] fn scale_matrix_only_scales_translation() { let matrix = Matrix::from_elements([1.0, 2.0, 3.0, 4.0, 5.0, -6.0]); assert_eq!( scale_matrix(matrix, 10.0).elements(), [1.0, 2.0, 3.0, 4.0, 50.0, -60.0] ); } #[test] fn compute_tuple_deltas_no_variation_index_is_noop_after_resize() { let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap(); let varc = font.varc().unwrap(); let store = varc.multi_var_store().unwrap().unwrap(); let regions = store.region_list().unwrap(); let mut cache = ScalarCache::new(regions.region_count() as usize); let mut out = DeltaVec::new(); out.push(42.0); compute_tuple_deltas( &store, ®ions, NO_VARIATION_INDEX, &[coord(0.0)], 3, &mut cache, &mut out, ) .unwrap(); assert_eq!(out.as_slice(), &[0.0, 0.0, 0.0]); compute_tuple_deltas( &store, ®ions, NO_VARIATION_INDEX, &[coord(0.0)], 0, &mut cache, &mut out, ) .unwrap(); assert!(out.is_empty()); } #[test] fn compute_tuple_deltas_invalid_outer_index_errors() { let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap(); let varc = font.varc().unwrap(); let store = varc.multi_var_store().unwrap().unwrap(); let regions = store.region_list().unwrap(); let mut cache = ScalarCache::new(regions.region_count() as usize); let mut out = DeltaVec::new(); let err = compute_tuple_deltas(&store, ®ions, 0xFFFF_0000, &[], 1, &mut cache, &mut out) .unwrap_err(); assert!(matches!(err, ReadError::InvalidCollectionIndex(_))); } #[test] fn compute_tuple_deltas_matches_manual_decode() { let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap(); let varc = font.varc().unwrap(); let store = varc.multi_var_store().unwrap().unwrap(); let regions = store.region_list().unwrap(); let region_list = regions.regions(); let coords = [coord(0.5); 8]; let mut tried = 0usize; for (outer, data) in store.variation_data().iter().enumerate() { let data = data.unwrap(); let region_count = data.region_indices().len(); if region_count == 0 { continue; } let delta_set_count = data.delta_sets().unwrap().count() as usize; for inner in 0..delta_set_count.min(3) { let decoded = data.delta_set(inner).unwrap().iter().collect::>(); if decoded.is_empty() || decoded.len() % region_count != 0 { continue; } let tuple_len = decoded.len() / region_count; let var_idx = ((outer as u32) << 16) | inner as u32; let mut cache = ScalarCache::new(regions.region_count() as usize); let mut actual = DeltaVec::new(); compute_tuple_deltas( &store, ®ions, var_idx, &coords, tuple_len, &mut cache, &mut actual, ) .unwrap(); let first = actual.as_slice().to_vec(); // Same cache after population should not alter results. compute_tuple_deltas( &store, ®ions, var_idx, &coords, tuple_len, &mut cache, &mut actual, ) .unwrap(); assert_eq!(actual.as_slice(), first.as_slice()); let mut expected = vec![0.0f32; tuple_len]; for (region_order, region_idx) in data.region_indices().iter().enumerate() { let scalar = region_list .get(region_idx.get() as usize) .unwrap() .compute_scalar_f32(&coords); if scalar == 0.0 { continue; } let base = region_order * tuple_len; for (i, slot) in expected.iter_mut().enumerate() { *slot += decoded[base + i] as f32 * scalar; } } assert_eq!(actual.len(), expected.len()); for (a, e) in actual.iter().zip(expected.iter()) { assert_close(*a, *e); } tried += 1; } } assert!(tried > 0, "expected at least one tuple to be exercised"); } fn apply_transform_variations_reference( component: &VarcComponent<'_>, coords: &[F2Dot14], transform: &mut DecomposedTransform, var_store: Option<&MultiItemVariationStore<'_>>, regions: Option<&SparseVariationRegionList<'_>>, scalar_cache: &mut ScalarCache, deltas: &mut DeltaVec, ) -> Result<(), DrawError> { let Some(var_idx) = component.transform_var_index() else { return Ok(()); }; let flags = component.flags(); const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate( VarcFlags::HAVE_TRANSLATE_X.bits() | VarcFlags::HAVE_TRANSLATE_Y.bits() | VarcFlags::HAVE_ROTATION.bits() | VarcFlags::HAVE_SCALE_X.bits() | VarcFlags::HAVE_SCALE_Y.bits() | VarcFlags::HAVE_SKEW_X.bits() | VarcFlags::HAVE_SKEW_Y.bits() | VarcFlags::HAVE_TCENTER_X.bits() | VarcFlags::HAVE_TCENTER_Y.bits(), ); let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize; if field_count == 0 { return Ok(()); } let store = var_store.ok_or(ReadError::NullOffset)?; let regions = regions.ok_or(ReadError::NullOffset)?; compute_tuple_deltas( store, regions, var_idx, coords, field_count, scalar_cache, deltas, )?; let mut delta_iter = deltas.iter().copied(); if flags.contains(VarcFlags::HAVE_TRANSLATE_X) { transform.set_translate_x(transform.translate_x() + delta_iter.next().unwrap_or(0.0)); } if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) { transform.set_translate_y(transform.translate_y() + delta_iter.next().unwrap_or(0.0)); } if flags.contains(VarcFlags::HAVE_ROTATION) { transform .set_rotation(transform.rotation() + delta_iter.next().unwrap_or(0.0) / 4096.0); } if flags.contains(VarcFlags::HAVE_SCALE_X) { transform.set_scale_x(transform.scale_x() + delta_iter.next().unwrap_or(0.0) / 1024.0); } if flags.contains(VarcFlags::HAVE_SCALE_Y) { transform.set_scale_y(transform.scale_y() + delta_iter.next().unwrap_or(0.0) / 1024.0); } const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate( VarcFlags::HAVE_SKEW_X.bits() | VarcFlags::HAVE_SKEW_Y.bits() | VarcFlags::HAVE_TCENTER_X.bits() | VarcFlags::HAVE_TCENTER_Y.bits(), ); if flags.intersects(SKEW_OR_CENTER) { if flags.contains(VarcFlags::HAVE_SKEW_X) { transform .set_skew_x(transform.skew_x() + delta_iter.next().unwrap_or(0.0) / 4096.0); } if flags.contains(VarcFlags::HAVE_SKEW_Y) { transform .set_skew_y(transform.skew_y() + delta_iter.next().unwrap_or(0.0) / 4096.0); } if flags.contains(VarcFlags::HAVE_TCENTER_X) { transform.set_center_x(transform.center_x() + delta_iter.next().unwrap_or(0.0)); } if flags.contains(VarcFlags::HAVE_TCENTER_Y) { transform.set_center_y(transform.center_y() + delta_iter.next().unwrap_or(0.0)); } } if !flags.contains(VarcFlags::HAVE_SCALE_Y) { transform.set_scale_y(transform.scale_x()); } Ok(()) } #[test] fn apply_transform_variations_matches_reference_path() { let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap(); let outlines = Outlines::new(&font).unwrap(); let coverage = outlines.varc.coverage().unwrap(); let var_store = outlines.varc.multi_var_store().transpose().unwrap(); let regions = var_store .as_ref() .map(|s| s.region_list()) .transpose() .unwrap(); let region_count = regions .as_ref() .map(|r| r.region_count() as usize) .unwrap_or(0); let mut coords = CoordVec::new(); coords.resize_and_fill(outlines.axis_count, F2Dot14::ZERO); for (i, c) in coords.iter_mut().enumerate() { *c = match i % 4 { 0 => coord(0.5), 1 => coord(-0.5), 2 => coord(0.25), _ => coord(-0.25), }; } let mut tested = 0usize; for gid16 in coverage.iter() { let gid: GlyphId = gid16.into(); let coverage_index = coverage.get(gid).unwrap() as usize; let glyph = outlines.varc.glyph(coverage_index).unwrap(); for component in glyph.components() { let component = component.unwrap(); if component.transform_var_index().is_none() { continue; } let mut transform_new = *component.transform(); let mut transform_ref = *component.transform(); let mut cache_new = ScalarCache::new(region_count); let mut cache_ref = ScalarCache::new(region_count); let mut deltas_ref = DeltaVec::new(); let mut scratch = Scratchpad::new(); let store_regions = var_store.as_ref().zip(regions.as_ref()); outlines .apply_transform_variations( &component, &coords, &mut transform_new, &mut cache_new, &mut scratch, store_regions, ) .unwrap(); apply_transform_variations_reference( &component, &coords, &mut transform_ref, var_store.as_ref(), regions.as_ref(), &mut cache_ref, &mut deltas_ref, ) .unwrap(); assert_close(transform_new.translate_x(), transform_ref.translate_x()); assert_close(transform_new.translate_y(), transform_ref.translate_y()); assert_close(transform_new.rotation(), transform_ref.rotation()); assert_close(transform_new.scale_x(), transform_ref.scale_x()); assert_close(transform_new.scale_y(), transform_ref.scale_y()); assert_close(transform_new.skew_x(), transform_ref.skew_x()); assert_close(transform_new.skew_y(), transform_ref.skew_y()); assert_close(transform_new.center_x(), transform_ref.center_x()); assert_close(transform_new.center_y(), transform_ref.center_y()); tested += 1; if tested >= 32 { break; } } if tested >= 32 { break; } } assert!(tested > 0, "expected at least one transformed component"); } #[test] fn draw_varc_6868_freetype_path_head_snapshot() { let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap(); let outlines = Outlines::new(&font).unwrap(); let gid = font.cmap().unwrap().map_codepoint(0x6868_u32).unwrap(); let outline = outlines.outline(gid).unwrap().unwrap(); let mut memory = vec![0u8; outline.required_buffer_size()]; let mut pen = Vec::::new(); outlines .draw( &outline, &mut memory, Size::unscaled(), &[], PathStyle::FreeType, &mut pen, ) .unwrap(); let head = path_head_signature(&pen, 8); assert_eq!( head, vec![ "M454.56,574.77".to_string(), "Q477.58,585.56 499.80,598.25".to_string(), "Q522.02,610.95 543.36,625.16".to_string(), "Q564.71,639.37 584.54,655.19".to_string(), "Q604.38,671.02 623.03,688.41".to_string(), "Q641.67,705.80 658.41,724.46".to_string(), "Q675.14,743.12 689.79,763.21".to_string(), "Q704.43,783.31 717.03,804.56".to_string(), ] ); } }