From 415f9c4dc80650087473f5a327cfe288d79ccc32 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 8 Nov 2023 22:26:00 -0500 Subject: [PATCH 1/7] [skrifa] floating point COLR Internal interface for color outline loading in full floating point. --- read-fonts/src/tables/variations.rs | 84 ++++ skrifa/src/scale/colr/instance.rs | 653 ++++++++++++++++++++++++++++ skrifa/src/scale/colr/mod.rs | 4 + skrifa/src/scale/colr/print.rs | 149 +++++++ skrifa/src/scale/mod.rs | 3 + 5 files changed, 893 insertions(+) create mode 100644 skrifa/src/scale/colr/instance.rs create mode 100644 skrifa/src/scale/colr/mod.rs create mode 100644 skrifa/src/scale/colr/print.rs diff --git a/read-fonts/src/tables/variations.rs b/read-fonts/src/tables/variations.rs index ec2403abd..7ed90cec6 100644 --- a/read-fonts/src/tables/variations.rs +++ b/read-fonts/src/tables/variations.rs @@ -577,6 +577,66 @@ impl<'a> ItemVariationStore<'a> { } Ok(((accum + 0x8000) >> 16) as i32) } + + /// Computes the delta value in floating point for the specified index and set + /// of normalized variation coordinates. + /// + /// This requires a type parameter specifying the type of target value for the + /// delta. + pub fn compute_delta_f32( + &self, + index: DeltaSetIndex, + coords: &[F2Dot14], + ) -> Result { + let data = match self.item_variation_data().get(index.outer as usize) { + Some(data) => data?, + None => return Ok(0.0), + }; + let regions = self.variation_region_list()?.variation_regions(); + let region_indices = data.region_indexes(); + // Compute deltas in 32-bit floating point. + let mut accum = 0f32; + for (i, region_delta) in data.delta_set(index.inner).enumerate() { + let region_index = region_indices + .get(i) + .ok_or(ReadError::MalformedData( + "invalid delta sets in ItemVariationStore", + ))? + .get() as usize; + let region = regions.get(region_index)?; + let scalar = region.compute_scalar_f32(coords); + accum += T::delta_to_f32(region_delta) * scalar; + } + Ok(accum) + } +} + +pub trait ItemDeltaTarget { + fn delta_to_f32(delta: i32) -> f32; +} + +impl ItemDeltaTarget for Fixed { + fn delta_to_f32(delta: i32) -> f32 { + Fixed::from_bits(delta).to_f32() + } +} + +impl ItemDeltaTarget for FWord { + fn delta_to_f32(delta: i32) -> f32 { + delta as f32 + } +} + +impl ItemDeltaTarget for UfWord { + fn delta_to_f32(delta: i32) -> f32 { + delta as f32 + } +} + +impl ItemDeltaTarget for F2Dot14 { + fn delta_to_f32(delta: i32) -> f32 { + F2Dot14::from_bits(delta as i16).to_f32() + } } impl<'a> VariationRegion<'a> { @@ -604,6 +664,30 @@ impl<'a> VariationRegion<'a> { } scalar } + + /// Computes a floating point scalar value for this region and the + /// specified normalized variation coordinates. + pub fn compute_scalar_f32(&self, coords: &[F2Dot14]) -> f32 { + let mut scalar = 1.0; + for (i, axis_coords) in self.region_axes().iter().enumerate() { + let coord = coords.get(i).map(|coord| coord.to_f32()).unwrap_or(0.0); + let start = axis_coords.start_coord.get().to_f32(); + let end = axis_coords.end_coord.get().to_f32(); + let peak = axis_coords.peak_coord.get().to_f32(); + if start > peak || peak > end || peak == 0.0 || start < 0.0 && end > 0.0 { + continue; + } else if coord < start || coord > end { + return 0.0; + } else if coord == peak { + continue; + } else if coord < peak { + scalar = (scalar * (coord - start)) / (peak - start); + } else { + scalar = (scalar * (end - coord)) / (end - peak); + } + } + scalar + } } impl<'a> ItemVariationData<'a> { diff --git a/skrifa/src/scale/colr/instance.rs b/skrifa/src/scale/colr/instance.rs new file mode 100644 index 000000000..e78bd3ce5 --- /dev/null +++ b/skrifa/src/scale/colr/instance.rs @@ -0,0 +1,653 @@ +//! COLR table instance. + +use read_fonts::{ + tables::{ + colr::*, + variations::{DeltaSetIndex, DeltaSetIndexMap, ItemVariationStore}, + }, + types::{BoundingBox, F2Dot14, FWord, Fixed, GlyphId, Point, UfWord}, + ReadError, +}; + +use core::ops::{Deref, Range}; + +/// Identifier used for representing a paint on the recursion blacklist. +pub type PaintId = usize; + +/// Combination of a `COLR` table and a location in variation space for +/// resolving paints. +/// +/// See [`Paint::resolve`], [`ColorStops::resolve`] and [`ClipBox::resolve`]. +pub struct ColrInstance<'a> { + colr: Colr<'a>, + index_map: Option>, + var_store: Option>, + coords: &'a [F2Dot14], +} + +impl<'a> ColrInstance<'a> { + /// Creates a new instance for the given `COLR` table and normalized variation + /// coordinates. + pub fn new(colr: Colr<'a>, coords: &'a [F2Dot14]) -> Self { + let index_map = colr.var_index_map().and_then(|res| res.ok()); + let var_store = colr.item_variation_store().and_then(|res| res.ok()); + Self { + colr, + coords, + index_map, + var_store, + } + } + + /// Computes a sequence of N variation deltas starting at the given + /// `var_base` index. + fn var_deltas(&self, var_index_base: u32, tys: [DeltaTy; N]) -> [f32; N] { + // Magic value that indicates deltas should not be applied. + const NO_VARIATION_DELTAS: u32 = 0xFFFFFFFF; + // Note: FreeType never returns an error for these lookups, so + // we do the same and just `unwrap_or_default` on var store + // errors. + // See + let mut deltas = [0.0; N]; + if self.coords.is_empty() + || self.var_store.is_none() + || var_index_base == NO_VARIATION_DELTAS + { + return deltas; + } + let var_store = self.var_store.as_ref().unwrap(); + let compute_delta = |ty: &DeltaTy, delta_ix| { + match ty { + DeltaTy::FWord => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaTy::UfWord => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaTy::Fixed => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaTy::F2Dot14 => var_store.compute_delta_f32::(delta_ix, self.coords), + } + .unwrap_or_default() + }; + if let Some(index_map) = self.index_map.as_ref() { + for (i, (ty, delta)) in tys.iter().zip(deltas.iter_mut()).enumerate() { + let var_index = var_index_base + i as u32; + if let Ok(delta_ix) = index_map.get(var_index) { + *delta = compute_delta(ty, delta_ix); + } + } + } else { + for (i, (ty, delta)) in tys.iter().zip(deltas.iter_mut()).enumerate() { + let var_index = var_index_base + i as u32; + // If we don't have a var index map, use our index as the inner + // component and set the outer to 0. + let delta_ix = DeltaSetIndex { + outer: 0, + inner: var_index as u16, + }; + *delta = compute_delta(ty, delta_ix); + } + } + deltas + } +} + +#[derive(Copy, Clone)] +enum DeltaTy { + FWord, + UfWord, + Fixed, + F2Dot14, +} + +impl<'a> Deref for ColrInstance<'a> { + type Target = Colr<'a>; + + fn deref(&self) -> &Self::Target { + &self.colr + } +} + +/// Resolves a clip box, applying variation deltas using the given +/// instance. +pub fn resolve_clip_box(instance: &ColrInstance, clip_box: &ClipBox) -> BoundingBox { + match clip_box { + ClipBox::Format1(cbox) => BoundingBox { + x_min: cbox.x_min().to_i16() as f32, + y_min: cbox.y_min().to_i16() as f32, + x_max: cbox.x_max().to_i16() as f32, + y_max: cbox.y_max().to_i16() as f32, + }, + ClipBox::Format2(cbox) => { + let deltas = instance.var_deltas(cbox.var_index_base(), [DeltaTy::FWord; 4]); + BoundingBox { + x_min: cbox.x_min().apply_delta(deltas[0]), + y_min: cbox.y_min().apply_delta(deltas[1]), + x_max: cbox.x_max().apply_delta(deltas[2]), + y_max: cbox.y_max().apply_delta(deltas[3]), + } + } + } +} + +/// Simplified version of a [`ColorStop`] or [`VarColorStop`] with applied +/// variation deltas. +#[derive(Clone, Debug)] +pub struct ResolvedColorStop { + pub offset: f32, + pub palette_index: u16, + pub alpha: f32, +} + +/// Collection of [`ColorStop`] or [`VarColorStop`]. +// Note: only one of these fields is used at any given time, but this structure +// was chosen over the obvious enum approach for simplicity in generating a +// single concrete type for the `impl Iterator` return type of the `resolve` +// method. +pub struct ColorStops<'a> { + stops: &'a [ColorStop], + var_stops: &'a [VarColorStop], +} + +impl<'a> From> for ColorStops<'a> { + fn from(value: ColorLine<'a>) -> Self { + Self { + stops: value.color_stops(), + var_stops: &[], + } + } +} + +impl<'a> From> for ColorStops<'a> { + fn from(value: VarColorLine<'a>) -> Self { + Self { + stops: &[], + var_stops: value.color_stops(), + } + } +} + +impl<'a> ColorStops<'a> { + /// Returns an iterator yielding resolved color stops with variation deltas + /// applied. + pub fn resolve( + &self, + instance: &'a ColrInstance<'a>, + ) -> impl Iterator + 'a { + self.stops + .iter() + .map(|stop| ResolvedColorStop { + offset: stop.stop_offset().to_f32(), + palette_index: stop.palette_index(), + alpha: stop.alpha().to_f32(), + }) + .chain(self.var_stops.iter().map(|stop| { + let deltas = instance.var_deltas(stop.var_index_base(), [DeltaTy::F2Dot14; 2]); + ResolvedColorStop { + offset: stop.stop_offset().apply_delta(deltas[0]), + palette_index: stop.palette_index(), + alpha: stop.alpha().apply_delta(deltas[1]), + } + })) + } +} + +/// Simplified version of `Paint` with applied variation deltas. +/// +/// These are constructed with the [`resolve_paint`] function. +/// +/// This is roughly equivalent to FreeType's +/// [`FT_COLR_Paint`](https://freetype.org/freetype2/docs/reference/ft2-layer_management.html#ft_colr_paint) +/// type. +pub enum ResolvedPaint<'a> { + ColrLayers { + range: Range, + }, + Solid { + palette_index: u16, + alpha: f32, + }, + LinearGradient { + x0: f32, + y0: f32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + color_stops: ColorStops<'a>, + extend: Extend, + }, + RadialGradient { + x0: f32, + y0: f32, + radius0: f32, + x1: f32, + y1: f32, + radius1: f32, + color_stops: ColorStops<'a>, + extend: Extend, + }, + SweepGradient { + center_x: f32, + center_y: f32, + start_angle: f32, + end_angle: f32, + color_stops: ColorStops<'a>, + extend: Extend, + }, + Glyph { + glyph_id: GlyphId, + paint: Paint<'a>, + }, + ColrGlyph { + glyph_id: GlyphId, + }, + Transform { + xx: f32, + yx: f32, + xy: f32, + yy: f32, + dx: f32, + dy: f32, + paint: Paint<'a>, + }, + Translate { + dx: f32, + dy: f32, + paint: Paint<'a>, + }, + Scale { + scale_x: f32, + scale_y: f32, + around_center: Option>, + paint: Paint<'a>, + }, + Rotate { + angle: f32, + around_center: Option>, + paint: Paint<'a>, + }, + Skew { + x_skew_angle: f32, + y_skew_angle: f32, + around_center: Option>, + paint: Paint<'a>, + }, + Composite { + source_paint: Paint<'a>, + mode: CompositeMode, + backdrop_paint: Paint<'a>, + }, +} + +/// Resolves this paint with the given instance. +/// +/// Resolving means that all numeric values are converted to 16.16 fixed +/// point, variation deltas are applied, and the various transform +/// paints are collapsed into a single value for their category (transform, +/// translate, scale, rotate and skew). +/// +/// This provides a simpler type for consumers that are more interested +/// in extracting the semantics of the graph rather than working with the +/// raw encoded structures. +pub fn resolve_paint<'a>( + instance: &ColrInstance<'a>, + paint: &Paint<'a>, +) -> Result, ReadError> { + Ok(match paint { + Paint::ColrLayers(layers) => { + let start = layers.first_layer_index() as usize; + ResolvedPaint::ColrLayers { + range: start..start + layers.num_layers() as usize, + } + } + Paint::Solid(solid) => ResolvedPaint::Solid { + palette_index: solid.palette_index(), + alpha: solid.alpha().to_f32(), + }, + Paint::VarSolid(solid) => { + let deltas = instance.var_deltas(solid.var_index_base(), [DeltaTy::F2Dot14]); + ResolvedPaint::Solid { + palette_index: solid.palette_index(), + alpha: solid.alpha().apply_delta(deltas[0]), + } + } + Paint::LinearGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + ResolvedPaint::LinearGradient { + x0: gradient.x0().to_i16() as f32, + y0: gradient.y0().to_i16() as f32, + x1: gradient.x1().to_i16() as f32, + y1: gradient.y1().to_i16() as f32, + x2: gradient.x2().to_i16() as f32, + y2: gradient.y2().to_i16() as f32, + color_stops: color_line.into(), + extend, + } + } + Paint::VarLinearGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaTy::FWord; 6]); + ResolvedPaint::LinearGradient { + x0: gradient.x0().apply_delta(deltas[0]), + y0: gradient.y0().apply_delta(deltas[1]), + x1: gradient.x1().apply_delta(deltas[2]), + y1: gradient.y1().apply_delta(deltas[3]), + x2: gradient.x2().apply_delta(deltas[4]), + y2: gradient.y2().apply_delta(deltas[5]), + color_stops: color_line.into(), + extend, + } + } + Paint::RadialGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + ResolvedPaint::RadialGradient { + x0: gradient.x0().to_i16() as f32, + y0: gradient.y0().to_i16() as f32, + radius0: gradient.radius0().to_u16() as f32, + x1: gradient.x1().to_i16() as f32, + y1: gradient.y1().to_i16() as f32, + radius1: gradient.radius1().to_u16() as f32, + color_stops: color_line.into(), + extend, + } + } + Paint::VarRadialGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaTy::FWord; 6]); + ResolvedPaint::RadialGradient { + x0: gradient.x0().apply_delta(deltas[0]), + y0: gradient.y0().apply_delta(deltas[1]), + radius0: gradient.radius0().apply_delta(deltas[2]), + x1: gradient.x1().apply_delta(deltas[3]), + y1: gradient.y1().apply_delta(deltas[4]), + radius1: gradient.radius1().apply_delta(deltas[5]), + color_stops: color_line.into(), + extend, + } + } + Paint::SweepGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + ResolvedPaint::SweepGradient { + center_x: gradient.center_x().to_i16() as f32, + center_y: gradient.center_y().to_i16() as f32, + start_angle: gradient.start_angle().to_f32(), + end_angle: gradient.end_angle().to_f32(), + color_stops: color_line.into(), + extend, + } + } + Paint::VarSweepGradient(gradient) => { + let color_line = gradient.color_line()?; + let extend = color_line.extend(); + let deltas = instance.var_deltas( + gradient.var_index_base(), + [ + DeltaTy::FWord, + DeltaTy::FWord, + DeltaTy::F2Dot14, + DeltaTy::F2Dot14, + ], + ); + ResolvedPaint::SweepGradient { + center_x: gradient.center_x().apply_delta(deltas[0]), + center_y: gradient.center_y().apply_delta(deltas[1]), + start_angle: gradient.start_angle().apply_delta(deltas[2]), + end_angle: gradient.end_angle().apply_delta(deltas[3]), + color_stops: color_line.into(), + extend, + } + } + Paint::Glyph(glyph) => ResolvedPaint::Glyph { + glyph_id: glyph.glyph_id(), + paint: glyph.paint()?, + }, + Paint::ColrGlyph(glyph) => ResolvedPaint::ColrGlyph { + glyph_id: glyph.glyph_id(), + }, + Paint::Transform(transform) => { + let affine = transform.transform()?; + let paint = transform.paint()?; + ResolvedPaint::Transform { + xx: affine.xx().to_f32(), + yx: affine.yx().to_f32(), + xy: affine.xy().to_f32(), + yy: affine.yy().to_f32(), + dx: affine.dx().to_f32(), + dy: affine.dy().to_f32(), + paint, + } + } + Paint::VarTransform(transform) => { + let affine = transform.transform()?; + let paint = transform.paint()?; + let deltas = instance.var_deltas(affine.var_index_base(), [DeltaTy::Fixed; 6]); + ResolvedPaint::Transform { + xx: affine.xx().apply_delta(deltas[0]), + yx: affine.yx().apply_delta(deltas[1]), + xy: affine.xy().apply_delta(deltas[2]), + yy: affine.yy().apply_delta(deltas[3]), + dx: affine.dx().apply_delta(deltas[4]), + dy: affine.dy().apply_delta(deltas[5]), + paint, + } + } + Paint::Translate(transform) => ResolvedPaint::Translate { + dx: transform.dx().to_i16() as f32, + dy: transform.dy().to_i16() as f32, + paint: transform.paint()?, + }, + Paint::VarTranslate(transform) => { + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::FWord; 2]); + ResolvedPaint::Translate { + dx: transform.dx().apply_delta(deltas[0]), + dy: transform.dy().apply_delta(deltas[1]), + paint: transform.paint()?, + } + } + Paint::Scale(transform) => ResolvedPaint::Scale { + scale_x: transform.scale_x().to_f32(), + scale_y: transform.scale_y().to_f32(), + around_center: None, + paint: transform.paint()?, + }, + Paint::VarScale(transform) => { + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14; 2]); + ResolvedPaint::Scale { + scale_x: transform.scale_x().apply_delta(deltas[0]), + scale_y: transform.scale_y().apply_delta(deltas[1]), + around_center: None, + paint: transform.paint()?, + } + } + Paint::ScaleAroundCenter(transform) => ResolvedPaint::Scale { + scale_x: transform.scale_x().to_f32(), + scale_y: transform.scale_y().to_f32(), + around_center: Some(Point::new( + transform.center_x().to_i16() as f32, + transform.center_y().to_i16() as f32, + )), + paint: transform.paint()?, + }, + Paint::VarScaleAroundCenter(transform) => { + let deltas = instance.var_deltas( + transform.var_index_base(), + [ + DeltaTy::F2Dot14, + DeltaTy::F2Dot14, + DeltaTy::FWord, + DeltaTy::FWord, + ], + ); + ResolvedPaint::Scale { + scale_x: transform.scale_x().apply_delta(deltas[0]), + scale_y: transform.scale_y().apply_delta(deltas[1]), + around_center: Some(Point::new( + transform.center_x().apply_delta(deltas[2]), + transform.center_y().apply_delta(deltas[3]), + )), + paint: transform.paint()?, + } + } + Paint::ScaleUniform(transform) => { + let scale = transform.scale().to_f32(); + ResolvedPaint::Scale { + scale_x: scale, + scale_y: scale, + around_center: None, + paint: transform.paint()?, + } + } + Paint::VarScaleUniform(transform) => { + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14]); + let scale = transform.scale().apply_delta(deltas[0]); + ResolvedPaint::Scale { + scale_x: scale, + scale_y: scale, + around_center: None, + paint: transform.paint()?, + } + } + Paint::ScaleUniformAroundCenter(transform) => { + let scale = transform.scale().to_f32(); + ResolvedPaint::Scale { + scale_x: scale, + scale_y: scale, + around_center: Some(Point::new( + transform.center_x().to_i16() as f32, + transform.center_y().to_i16() as f32, + )), + paint: transform.paint()?, + } + } + Paint::VarScaleUniformAroundCenter(transform) => { + let deltas = instance.var_deltas( + transform.var_index_base(), + [DeltaTy::F2Dot14, DeltaTy::FWord, DeltaTy::FWord], + ); + let scale = transform.scale().apply_delta(deltas[0]); + ResolvedPaint::Scale { + scale_x: scale, + scale_y: scale, + around_center: Some(Point::new( + transform.center_x().apply_delta(deltas[1]), + transform.center_y().apply_delta(deltas[2]), + )), + paint: transform.paint()?, + } + } + Paint::Rotate(transform) => ResolvedPaint::Rotate { + angle: transform.angle().to_f32(), + around_center: None, + paint: transform.paint()?, + }, + Paint::VarRotate(transform) => { + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14]); + ResolvedPaint::Rotate { + angle: transform.angle().apply_delta(deltas[0]), + around_center: None, + paint: transform.paint()?, + } + } + Paint::RotateAroundCenter(transform) => ResolvedPaint::Rotate { + angle: transform.angle().to_f32(), + around_center: Some(Point::new( + transform.center_x().to_i16() as f32, + transform.center_y().to_i16() as f32, + )), + paint: transform.paint()?, + }, + Paint::VarRotateAroundCenter(transform) => { + let deltas = instance.var_deltas( + transform.var_index_base(), + [DeltaTy::F2Dot14, DeltaTy::FWord, DeltaTy::FWord], + ); + ResolvedPaint::Rotate { + angle: transform.angle().apply_delta(deltas[0]), + around_center: Some(Point::new( + transform.center_x().apply_delta(deltas[1]), + transform.center_y().apply_delta(deltas[2]), + )), + paint: transform.paint()?, + } + } + Paint::Skew(transform) => ResolvedPaint::Skew { + x_skew_angle: transform.x_skew_angle().to_f32(), + y_skew_angle: transform.y_skew_angle().to_f32(), + around_center: None, + paint: transform.paint()?, + }, + Paint::VarSkew(transform) => { + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14; 2]); + ResolvedPaint::Skew { + x_skew_angle: transform.x_skew_angle().apply_delta(deltas[0]), + y_skew_angle: transform.y_skew_angle().apply_delta(deltas[1]), + around_center: None, + paint: transform.paint()?, + } + } + Paint::SkewAroundCenter(transform) => ResolvedPaint::Skew { + x_skew_angle: transform.x_skew_angle().to_f32(), + y_skew_angle: transform.y_skew_angle().to_f32(), + around_center: Some(Point::new( + transform.center_x().to_i16() as f32, + transform.center_y().to_i16() as f32, + )), + paint: transform.paint()?, + }, + Paint::VarSkewAroundCenter(transform) => { + let deltas = instance.var_deltas( + transform.var_index_base(), + [ + DeltaTy::F2Dot14, + DeltaTy::F2Dot14, + DeltaTy::FWord, + DeltaTy::FWord, + ], + ); + ResolvedPaint::Skew { + x_skew_angle: transform.x_skew_angle().apply_delta(deltas[0]), + y_skew_angle: transform.y_skew_angle().apply_delta(deltas[1]), + around_center: Some(Point::new( + transform.center_x().apply_delta(deltas[2]), + transform.center_y().apply_delta(deltas[3]), + )), + paint: transform.paint()?, + } + } + Paint::Composite(composite) => ResolvedPaint::Composite { + source_paint: composite.source_paint()?, + mode: composite.composite_mode(), + backdrop_paint: composite.backdrop_paint()?, + }, + }) +} + +trait ApplyDelta { + fn apply_delta(self, delta: f32) -> f32; +} + +impl ApplyDelta for Fixed { + fn apply_delta(self, delta: f32) -> f32 { + self.to_f32() + delta + } +} + +impl ApplyDelta for F2Dot14 { + fn apply_delta(self, delta: f32) -> f32 { + self.to_f32() + delta + } +} + +impl ApplyDelta for FWord { + fn apply_delta(self, delta: f32) -> f32 { + self.to_i16() as f32 + delta + } +} + +impl ApplyDelta for UfWord { + fn apply_delta(self, delta: f32) -> f32 { + self.to_u16() as f32 + delta + } +} diff --git a/skrifa/src/scale/colr/mod.rs b/skrifa/src/scale/colr/mod.rs new file mode 100644 index 000000000..1a847e3b1 --- /dev/null +++ b/skrifa/src/scale/colr/mod.rs @@ -0,0 +1,4 @@ +mod instance; +mod print; + +pub use print::print_color_glyph; diff --git a/skrifa/src/scale/colr/print.rs b/skrifa/src/scale/colr/print.rs new file mode 100644 index 000000000..32aa3d44f --- /dev/null +++ b/skrifa/src/scale/colr/print.rs @@ -0,0 +1,149 @@ +use super::instance::{resolve_paint, ColorStops, ColrInstance, ResolvedPaint}; +use crate::prelude::{GlyphId, LocationRef}; +use read_fonts::tables::colr::{Colr, Paint}; + +pub fn print_color_glyph(colr: &Colr, glyph_id: GlyphId, location: LocationRef) { + let instance = ColrInstance::new(colr.clone(), location.coords()); + let paint = instance.v1_base_glyph(glyph_id).unwrap().unwrap().0; + print_paint(&instance, &paint, 0); +} + +fn print_paint(instance: &ColrInstance, paint: &Paint, depth: usize) { + let paint = resolve_paint(instance, paint).unwrap(); + for _ in 0..depth { + print!(" "); + } + match paint { + ResolvedPaint::Glyph { glyph_id, paint } => { + println!("Glyph {glyph_id}"); + print_paint(instance, &paint, depth + 1); + } + ResolvedPaint::ColrGlyph { glyph_id } => { + println!("ColrGlyph {glyph_id}"); + } + ResolvedPaint::ColrLayers { range } => { + println!("ColrLayers {:?}", range.clone()); + for i in range { + let paint = instance.v1_layer(i).unwrap().0; + print_paint(instance, &paint, depth + 1); + } + } + ResolvedPaint::Composite { + source_paint, + mode, + backdrop_paint, + } => { + println!("Composite {:?}", mode); + print_paint(instance, &backdrop_paint, depth + 1); + print_paint(instance, &source_paint, depth + 1); + } + ResolvedPaint::LinearGradient { + x0, + y0, + x1, + y1, + x2, + y2, + color_stops, + extend, + } => { + print!( + "LinearGradient p0: ({x0},{y0}) p1: ({x1},{y1}) p2: ({x2},{y2}) extend: {:?} ", + extend + ); + print_color_stops(instance, color_stops); + } + ResolvedPaint::RadialGradient { + x0, + y0, + radius0, + x1, + y1, + radius1, + color_stops, + extend, + } => { + print!("RadialGradient p0: ({x0},{y0}) r0: {radius0} p1: ({x1},{y1}) r1: {radius1} extend: {:?} ", extend); + print_color_stops(instance, color_stops); + } + ResolvedPaint::SweepGradient { + center_x, + center_y, + start_angle, + end_angle, + color_stops, + extend, + } => { + print!("SweepGradient center: ({center_x},{center_y}) angle: {start_angle}->{end_angle} extend: {:?} ", extend); + print_color_stops(instance, color_stops); + } + ResolvedPaint::Rotate { + angle, + around_center, + paint, + } => { + let center = around_center.unwrap_or_default(); + println!("Rotate angle: {angle} center: ({}, {})", center.x, center.y); + print_paint(instance, &paint, depth + 1); + } + ResolvedPaint::Scale { + scale_x, + scale_y, + around_center, + paint, + } => { + let center = around_center.unwrap_or_default(); + println!( + "Scale x: {scale_x} y: {scale_y} center: ({}, {})", + center.x, center.y + ); + print_paint(instance, &paint, depth + 1); + } + ResolvedPaint::Skew { + x_skew_angle, + y_skew_angle, + around_center, + paint, + } => { + let center = around_center.unwrap_or_default(); + println!( + "Skew x: {x_skew_angle} y: {y_skew_angle} center: ({}, {})", + center.x, center.y + ); + print_paint(instance, &paint, depth + 1); + } + ResolvedPaint::Solid { + palette_index, + alpha, + } => { + println!("Solid {palette_index} {alpha}"); + } + ResolvedPaint::Transform { + xx, + yx, + xy, + yy, + dx, + dy, + paint, + } => { + println!("Transform {xx} {yx} {xy} {yy} {dx} {dy}"); + print_paint(instance, &paint, depth + 1); + } + ResolvedPaint::Translate { dx, dy, paint } => { + println!("Translate x: {dx} y: {dy}"); + print_paint(instance, &paint, depth + 1); + } + } +} + +fn print_color_stops(instance: &ColrInstance, stops: ColorStops) { + print!("stops: "); + for (i, stop) in stops.resolve(instance).enumerate() { + if i > 0 { + print!(", "); + } + print!("({} {} {})", stop.offset, stop.palette_index, stop.alpha); + } + println!(); +} diff --git a/skrifa/src/scale/mod.rs b/skrifa/src/scale/mod.rs index 18ffb828d..5c7bebcd5 100644 --- a/skrifa/src/scale/mod.rs +++ b/skrifa/src/scale/mod.rs @@ -148,10 +148,13 @@ #![allow(dead_code)] mod cff; +mod colr; mod error; mod glyf; mod scaler; +pub use colr::print_color_glyph; + pub use read_fonts::types::Pen; pub use error::{Error, Result}; From ff6677fb04f68fd265b937199ec66619d789af1a Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 14 Nov 2023 14:11:19 -0500 Subject: [PATCH 2/7] cleanup to make ready for review - remove fixed point ColrInstance code from read-fonts - remove color paint debug printing module from skrifa --- read-fonts/src/tables/colr.rs | 607 +-------------------------------- skrifa/src/scale/colr/mod.rs | 6 +- skrifa/src/scale/colr/print.rs | 149 -------- skrifa/src/scale/mod.rs | 2 - 4 files changed, 5 insertions(+), 759 deletions(-) delete mode 100644 skrifa/src/scale/colr/print.rs diff --git a/read-fonts/src/tables/colr.rs b/read-fonts/src/tables/colr.rs index ae98eaba0..43c9a1836 100644 --- a/read-fonts/src/tables/colr.rs +++ b/read-fonts/src/tables/colr.rs @@ -1,9 +1,6 @@ //! The [COLR](https://docs.microsoft.com/en-us/typography/opentype/spec/colr) table -use super::variations::{DeltaSetIndex, DeltaSetIndexMap, ItemVariationStore}; -use types::BoundingBox; - -use std::ops::Deref; +use super::variations::{DeltaSetIndexMap, ItemVariationStore}; include!("../../generated/generated_colr.rs"); @@ -96,605 +93,3 @@ impl<'a> Colr<'a> { Ok(Some(clip.clip_box(list.offset_data())?)) } } - -/// Combination of a `COLR` table and a location in variation space for -/// resolving paints. -/// -/// See [`Paint::resolve`], [`ColorStops::resolve`] and [`ClipBox::resolve`]. -pub struct ColrInstance<'a> { - colr: Colr<'a>, - index_map: Option>, - var_store: Option>, - coords: &'a [F2Dot14], -} - -impl<'a> ColrInstance<'a> { - /// Creates a new instance for the given `COLR` table and normalized variation - /// coordinates. - pub fn new(colr: Colr<'a>, coords: &'a [F2Dot14]) -> Self { - let index_map = colr.var_index_map().and_then(|res| res.ok()); - let var_store = colr.item_variation_store().and_then(|res| res.ok()); - Self { - colr, - coords, - index_map, - var_store, - } - } - - /// Computes a sequence of N variation deltas starting at the given - /// `var_base` index. - fn var_deltas(&self, var_index_base: u32) -> [i32; N] { - // Magic value that indicates deltas should not be applied. - const NO_VARIATION_DELTAS: u32 = 0xFFFFFFFF; - // Note: FreeType never returns an error for these lookups, so - // we do the same and just `unwrap_or_default` on var store - // errors. - // See - let mut deltas = [0; N]; - if self.coords.is_empty() - || self.var_store.is_none() - || var_index_base == NO_VARIATION_DELTAS - { - return deltas; - } - let var_store = self.var_store.as_ref().unwrap(); - if let Some(index_map) = self.index_map.as_ref() { - for (i, delta) in deltas.iter_mut().enumerate() { - let var_index = var_index_base + i as u32; - *delta = index_map - .get(var_index) - .and_then(|delta_index| var_store.compute_delta(delta_index, self.coords)) - .unwrap_or_default(); - } - } else { - for (i, delta) in deltas.iter_mut().enumerate() { - let var_index = var_index_base + i as u32; - // If we don't have a var index map, use our index as the inner - // component and set the outer to 0. - let delta_index = DeltaSetIndex { - outer: 0, - inner: var_index as u16, - }; - *delta = var_store - .compute_delta(delta_index, self.coords) - .unwrap_or_default(); - } - } - deltas - } -} - -impl<'a> Deref for ColrInstance<'a> { - type Target = Colr<'a>; - - fn deref(&self) -> &Self::Target { - &self.colr - } -} - -impl<'a> ClipBox<'a> { - /// Resolves a clip box, applying variation deltas using the given - /// instance. - pub fn resolve(&self, instance: &ColrInstance<'a>) -> BoundingBox { - match self { - Self::Format1(cbox) => BoundingBox { - x_min: cbox.x_min().to_fixed(), - y_min: cbox.y_min().to_fixed(), - x_max: cbox.x_max().to_fixed(), - y_max: cbox.y_max().to_fixed(), - }, - Self::Format2(cbox) => { - let deltas = instance.var_deltas::<4>(cbox.var_index_base()); - BoundingBox { - x_min: cbox.x_min().apply_delta(deltas[0]), - y_min: cbox.y_min().apply_delta(deltas[1]), - x_max: cbox.x_max().apply_delta(deltas[2]), - y_max: cbox.y_max().apply_delta(deltas[3]), - } - } - } - } -} - -/// Simplified version of a [`ColorStop`] or [`VarColorStop`] with applied -/// variation deltas. -#[derive(Clone, Debug)] -pub struct ResolvedColorStop { - pub offset: Fixed, - pub palette_index: u16, - pub alpha: Fixed, -} - -/// Collection of [`ColorStop`] or [`VarColorStop`]. -// Note: only one of these fields is used at any given time, but this structure -// was chosen over the obvious enum approach for simplicity in generating a -// single concrete type for the `impl Iterator` return type of the `resolve` -// method. -pub struct ColorStops<'a> { - stops: &'a [ColorStop], - var_stops: &'a [VarColorStop], -} - -impl<'a> From> for ColorStops<'a> { - fn from(value: ColorLine<'a>) -> Self { - Self { - stops: value.color_stops(), - var_stops: &[], - } - } -} - -impl<'a> From> for ColorStops<'a> { - fn from(value: VarColorLine<'a>) -> Self { - Self { - stops: &[], - var_stops: value.color_stops(), - } - } -} - -impl<'a> ColorStops<'a> { - /// Returns an iterator yielding resolved color stops with variation deltas - /// applied. - pub fn resolve( - &self, - instance: &'a ColrInstance<'a>, - ) -> impl Iterator + 'a { - self.stops - .iter() - .map(|stop| ResolvedColorStop { - offset: stop.stop_offset().to_fixed(), - palette_index: stop.palette_index(), - alpha: stop.alpha().to_fixed(), - }) - .chain(self.var_stops.iter().map(|stop| { - let deltas = instance.var_deltas::<2>(stop.var_index_base()); - ResolvedColorStop { - offset: stop.stop_offset().apply_delta(deltas[0]), - palette_index: stop.palette_index(), - alpha: stop.alpha().apply_delta(deltas[1]), - } - })) - } -} - -/// Simplified version of `Paint` with applied variation deltas. -/// -/// These are constructed with [`Paint::resolve`] method. See the documentation -/// on that method for further detail. -/// -/// This is roughly equivalent to FreeType's -/// [`FT_COLR_Paint`](https://freetype.org/freetype2/docs/reference/ft2-layer_management.html#ft_colr_paint) -/// type. -pub enum ResolvedPaint<'a> { - ColrLayers { - range: Range, - }, - Solid { - palette_index: u16, - alpha: Fixed, - }, - LinearGradient { - x0: Fixed, - y0: Fixed, - x1: Fixed, - y1: Fixed, - x2: Fixed, - y2: Fixed, - color_stops: ColorStops<'a>, - extend: Extend, - }, - RadialGradient { - x0: Fixed, - y0: Fixed, - radius0: Fixed, - x1: Fixed, - y1: Fixed, - radius1: Fixed, - color_stops: ColorStops<'a>, - extend: Extend, - }, - SweepGradient { - center_x: Fixed, - center_y: Fixed, - start_angle: Fixed, - end_angle: Fixed, - color_stops: ColorStops<'a>, - extend: Extend, - }, - Glyph { - glyph_id: GlyphId, - paint: Paint<'a>, - }, - ColrGlyph { - glyph_id: GlyphId, - }, - Transform { - xx: Fixed, - yx: Fixed, - xy: Fixed, - yy: Fixed, - dx: Fixed, - dy: Fixed, - paint: Paint<'a>, - }, - Translate { - dx: Fixed, - dy: Fixed, - paint: Paint<'a>, - }, - Scale { - scale_x: Fixed, - scale_y: Fixed, - around_center: Option>, - paint: Paint<'a>, - }, - Rotate { - angle: Fixed, - around_center: Option>, - paint: Paint<'a>, - }, - Skew { - x_skew_angle: Fixed, - y_skew_angle: Fixed, - around_center: Option>, - paint: Paint<'a>, - }, - Composite { - source_paint: Paint<'a>, - mode: CompositeMode, - backdrop_paint: Paint<'a>, - }, -} - -impl<'a> Paint<'a> { - /// Resolves this paint with the given instance. - /// - /// Resolving means that all numeric values are converted to 16.16 fixed - /// point, variation deltas are applied, and the various transform - /// paints are collapsed into a single value for their category (transform, - /// translate, scale, rotate and skew). - /// - /// This provides a simpler type for consumers that are more interested - /// in extracting the semantics of the graph rather than working with the - /// raw encoded structures. - pub fn resolve(&self, instance: &ColrInstance<'a>) -> Result, ReadError> { - Ok(match self { - Self::ColrLayers(layers) => { - let start = layers.first_layer_index() as usize; - ResolvedPaint::ColrLayers { - range: start..start + layers.num_layers() as usize, - } - } - Self::Solid(solid) => ResolvedPaint::Solid { - palette_index: solid.palette_index(), - alpha: solid.alpha().to_fixed(), - }, - Self::VarSolid(solid) => { - let deltas = instance.var_deltas::<1>(solid.var_index_base()); - ResolvedPaint::Solid { - palette_index: solid.palette_index(), - alpha: solid.alpha().apply_delta(deltas[0]), - } - } - Self::LinearGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - ResolvedPaint::LinearGradient { - x0: gradient.x0().to_fixed(), - y0: gradient.y0().to_fixed(), - x1: gradient.x1().to_fixed(), - y1: gradient.y1().to_fixed(), - x2: gradient.x2().to_fixed(), - y2: gradient.y2().to_fixed(), - color_stops: color_line.into(), - extend, - } - } - Self::VarLinearGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - let deltas = instance.var_deltas::<6>(gradient.var_index_base()); - ResolvedPaint::LinearGradient { - x0: gradient.x0().apply_delta(deltas[0]), - y0: gradient.y0().apply_delta(deltas[1]), - x1: gradient.x1().apply_delta(deltas[2]), - y1: gradient.y1().apply_delta(deltas[3]), - x2: gradient.x2().apply_delta(deltas[4]), - y2: gradient.y2().apply_delta(deltas[5]), - color_stops: color_line.into(), - extend, - } - } - Self::RadialGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - ResolvedPaint::RadialGradient { - x0: gradient.x0().to_fixed(), - y0: gradient.y0().to_fixed(), - radius0: gradient.radius0().to_fixed(), - x1: gradient.x1().to_fixed(), - y1: gradient.y1().to_fixed(), - radius1: gradient.radius1().to_fixed(), - color_stops: color_line.into(), - extend, - } - } - Self::VarRadialGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - let deltas = instance.var_deltas::<6>(gradient.var_index_base()); - ResolvedPaint::RadialGradient { - x0: gradient.x0().apply_delta(deltas[0]), - y0: gradient.y0().apply_delta(deltas[1]), - radius0: gradient.radius0().apply_delta(deltas[2]), - x1: gradient.x1().apply_delta(deltas[3]), - y1: gradient.y1().apply_delta(deltas[4]), - radius1: gradient.radius1().apply_delta(deltas[5]), - color_stops: color_line.into(), - extend, - } - } - Self::SweepGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - ResolvedPaint::SweepGradient { - center_x: gradient.center_x().to_fixed(), - center_y: gradient.center_y().to_fixed(), - start_angle: gradient.start_angle().to_fixed(), - end_angle: gradient.end_angle().to_fixed(), - color_stops: color_line.into(), - extend, - } - } - Self::VarSweepGradient(gradient) => { - let color_line = gradient.color_line()?; - let extend = color_line.extend(); - let deltas = instance.var_deltas::<4>(gradient.var_index_base()); - ResolvedPaint::SweepGradient { - center_x: gradient.center_x().apply_delta(deltas[0]), - center_y: gradient.center_y().apply_delta(deltas[1]), - start_angle: gradient.start_angle().apply_delta(deltas[2]), - end_angle: gradient.end_angle().apply_delta(deltas[3]), - color_stops: color_line.into(), - extend, - } - } - Self::Glyph(glyph) => ResolvedPaint::Glyph { - glyph_id: glyph.glyph_id(), - paint: glyph.paint()?, - }, - Self::ColrGlyph(glyph) => ResolvedPaint::ColrGlyph { - glyph_id: glyph.glyph_id(), - }, - Self::Transform(transform) => { - let affine = transform.transform()?; - let paint = transform.paint()?; - ResolvedPaint::Transform { - xx: affine.xx(), - yx: affine.yx(), - xy: affine.xy(), - yy: affine.yy(), - dx: affine.dx(), - dy: affine.dy(), - paint, - } - } - Self::VarTransform(transform) => { - let affine = transform.transform()?; - let paint = transform.paint()?; - let deltas = instance.var_deltas::<6>(affine.var_index_base()); - ResolvedPaint::Transform { - xx: affine.xx().apply_delta(deltas[0]), - yx: affine.yx().apply_delta(deltas[1]), - xy: affine.xy().apply_delta(deltas[2]), - yy: affine.yy().apply_delta(deltas[3]), - dx: affine.dx().apply_delta(deltas[4]), - dy: affine.dy().apply_delta(deltas[5]), - paint, - } - } - Self::Translate(transform) => ResolvedPaint::Translate { - dx: transform.dx().to_fixed(), - dy: transform.dy().to_fixed(), - paint: transform.paint()?, - }, - Self::VarTranslate(transform) => { - let deltas = instance.var_deltas::<2>(transform.var_index_base()); - ResolvedPaint::Translate { - dx: transform.dx().apply_delta(deltas[0]), - dy: transform.dy().apply_delta(deltas[1]), - paint: transform.paint()?, - } - } - Self::Scale(transform) => ResolvedPaint::Scale { - scale_x: transform.scale_x().to_fixed(), - scale_y: transform.scale_y().to_fixed(), - around_center: None, - paint: transform.paint()?, - }, - Self::VarScale(transform) => { - let deltas = instance.var_deltas::<2>(transform.var_index_base()); - ResolvedPaint::Scale { - scale_x: transform.scale_x().apply_delta(deltas[0]), - scale_y: transform.scale_y().apply_delta(deltas[1]), - around_center: None, - paint: transform.paint()?, - } - } - Self::ScaleAroundCenter(transform) => ResolvedPaint::Scale { - scale_x: transform.scale_x().to_fixed(), - scale_y: transform.scale_y().to_fixed(), - around_center: Some(Point::new( - transform.center_x().to_fixed(), - transform.center_y().to_fixed(), - )), - paint: transform.paint()?, - }, - Self::VarScaleAroundCenter(transform) => { - let deltas = instance.var_deltas::<4>(transform.var_index_base()); - ResolvedPaint::Scale { - scale_x: transform.scale_x().apply_delta(deltas[0]), - scale_y: transform.scale_y().apply_delta(deltas[1]), - around_center: Some(Point::new( - transform.center_x().apply_delta(deltas[2]), - transform.center_y().apply_delta(deltas[3]), - )), - paint: transform.paint()?, - } - } - Self::ScaleUniform(transform) => { - let scale = transform.scale().to_fixed(); - ResolvedPaint::Scale { - scale_x: scale, - scale_y: scale, - around_center: None, - paint: transform.paint()?, - } - } - Self::VarScaleUniform(transform) => { - let deltas = instance.var_deltas::<1>(transform.var_index_base()); - let scale = transform.scale().apply_delta(deltas[0]); - ResolvedPaint::Scale { - scale_x: scale, - scale_y: scale, - around_center: None, - paint: transform.paint()?, - } - } - Self::ScaleUniformAroundCenter(transform) => { - let scale = transform.scale().to_fixed(); - ResolvedPaint::Scale { - scale_x: scale, - scale_y: scale, - around_center: Some(Point::new( - transform.center_x().to_fixed(), - transform.center_y().to_fixed(), - )), - paint: transform.paint()?, - } - } - Self::VarScaleUniformAroundCenter(transform) => { - let deltas = instance.var_deltas::<3>(transform.var_index_base()); - let scale = transform.scale().apply_delta(deltas[0]); - ResolvedPaint::Scale { - scale_x: scale, - scale_y: scale, - around_center: Some(Point::new( - transform.center_x().apply_delta(deltas[1]), - transform.center_y().apply_delta(deltas[2]), - )), - paint: transform.paint()?, - } - } - Self::Rotate(transform) => ResolvedPaint::Rotate { - angle: transform.angle().to_fixed(), - around_center: None, - paint: transform.paint()?, - }, - Self::VarRotate(transform) => { - let deltas = instance.var_deltas::<1>(transform.var_index_base()); - ResolvedPaint::Rotate { - angle: transform.angle().apply_delta(deltas[0]), - around_center: None, - paint: transform.paint()?, - } - } - Self::RotateAroundCenter(transform) => ResolvedPaint::Rotate { - angle: transform.angle().to_fixed(), - around_center: Some(Point::new( - transform.center_x().to_fixed(), - transform.center_y().to_fixed(), - )), - paint: transform.paint()?, - }, - Self::VarRotateAroundCenter(transform) => { - let deltas = instance.var_deltas::<3>(transform.var_index_base()); - ResolvedPaint::Rotate { - angle: transform.angle().apply_delta(deltas[0]), - around_center: Some(Point::new( - transform.center_x().apply_delta(deltas[1]), - transform.center_y().apply_delta(deltas[2]), - )), - paint: transform.paint()?, - } - } - Self::Skew(transform) => ResolvedPaint::Skew { - x_skew_angle: transform.x_skew_angle().to_fixed(), - y_skew_angle: transform.y_skew_angle().to_fixed(), - around_center: None, - paint: transform.paint()?, - }, - Self::VarSkew(transform) => { - let deltas = instance.var_deltas::<2>(transform.var_index_base()); - ResolvedPaint::Skew { - x_skew_angle: transform.x_skew_angle().apply_delta(deltas[0]), - y_skew_angle: transform.y_skew_angle().apply_delta(deltas[1]), - around_center: None, - paint: transform.paint()?, - } - } - Self::SkewAroundCenter(transform) => ResolvedPaint::Skew { - x_skew_angle: transform.x_skew_angle().to_fixed(), - y_skew_angle: transform.y_skew_angle().to_fixed(), - around_center: Some(Point::new( - transform.center_x().to_fixed(), - transform.center_y().to_fixed(), - )), - paint: transform.paint()?, - }, - Self::VarSkewAroundCenter(transform) => { - let deltas = instance.var_deltas::<4>(transform.var_index_base()); - ResolvedPaint::Skew { - x_skew_angle: transform.x_skew_angle().apply_delta(deltas[0]), - y_skew_angle: transform.y_skew_angle().apply_delta(deltas[1]), - around_center: Some(Point::new( - transform.center_x().apply_delta(deltas[2]), - transform.center_y().apply_delta(deltas[3]), - )), - paint: transform.paint()?, - } - } - Self::Composite(composite) => ResolvedPaint::Composite { - source_paint: composite.source_paint()?, - mode: composite.composite_mode(), - backdrop_paint: composite.backdrop_paint()?, - }, - }) - } -} - -/// Trait to augment all types in the `COLR` table with an appropriate -/// `apply_delta` method that both adds the delta and converts to 16.16 -/// fixed point. -/// -/// It might be worth moving this to `font-types` at some point, but -/// the conversion to `Fixed` may not be generally useful. -trait ApplyDelta { - fn apply_delta(self, delta: i32) -> Fixed; -} - -impl ApplyDelta for Fixed { - fn apply_delta(self, delta: i32) -> Fixed { - self + Fixed::from_bits(delta) - } -} - -impl ApplyDelta for F2Dot14 { - fn apply_delta(self, delta: i32) -> Fixed { - self.to_fixed() + F2Dot14::from_bits(delta as i16).to_fixed() - } -} - -impl ApplyDelta for FWord { - fn apply_delta(self, delta: i32) -> Fixed { - self.to_fixed() + Fixed::from_i32(delta) - } -} - -impl ApplyDelta for UfWord { - fn apply_delta(self, delta: i32) -> Fixed { - self.to_fixed() + Fixed::from_i32(delta) - } -} diff --git a/skrifa/src/scale/colr/mod.rs b/skrifa/src/scale/colr/mod.rs index 1a847e3b1..b92097a37 100644 --- a/skrifa/src/scale/colr/mod.rs +++ b/skrifa/src/scale/colr/mod.rs @@ -1,4 +1,6 @@ mod instance; -mod print; -pub use print::print_color_glyph; +pub use instance::{ + resolve_clip_box, resolve_paint, ColorStops, ColrInstance, PaintId, ResolvedColorStop, + ResolvedPaint, +}; diff --git a/skrifa/src/scale/colr/print.rs b/skrifa/src/scale/colr/print.rs deleted file mode 100644 index 32aa3d44f..000000000 --- a/skrifa/src/scale/colr/print.rs +++ /dev/null @@ -1,149 +0,0 @@ -use super::instance::{resolve_paint, ColorStops, ColrInstance, ResolvedPaint}; -use crate::prelude::{GlyphId, LocationRef}; -use read_fonts::tables::colr::{Colr, Paint}; - -pub fn print_color_glyph(colr: &Colr, glyph_id: GlyphId, location: LocationRef) { - let instance = ColrInstance::new(colr.clone(), location.coords()); - let paint = instance.v1_base_glyph(glyph_id).unwrap().unwrap().0; - print_paint(&instance, &paint, 0); -} - -fn print_paint(instance: &ColrInstance, paint: &Paint, depth: usize) { - let paint = resolve_paint(instance, paint).unwrap(); - for _ in 0..depth { - print!(" "); - } - match paint { - ResolvedPaint::Glyph { glyph_id, paint } => { - println!("Glyph {glyph_id}"); - print_paint(instance, &paint, depth + 1); - } - ResolvedPaint::ColrGlyph { glyph_id } => { - println!("ColrGlyph {glyph_id}"); - } - ResolvedPaint::ColrLayers { range } => { - println!("ColrLayers {:?}", range.clone()); - for i in range { - let paint = instance.v1_layer(i).unwrap().0; - print_paint(instance, &paint, depth + 1); - } - } - ResolvedPaint::Composite { - source_paint, - mode, - backdrop_paint, - } => { - println!("Composite {:?}", mode); - print_paint(instance, &backdrop_paint, depth + 1); - print_paint(instance, &source_paint, depth + 1); - } - ResolvedPaint::LinearGradient { - x0, - y0, - x1, - y1, - x2, - y2, - color_stops, - extend, - } => { - print!( - "LinearGradient p0: ({x0},{y0}) p1: ({x1},{y1}) p2: ({x2},{y2}) extend: {:?} ", - extend - ); - print_color_stops(instance, color_stops); - } - ResolvedPaint::RadialGradient { - x0, - y0, - radius0, - x1, - y1, - radius1, - color_stops, - extend, - } => { - print!("RadialGradient p0: ({x0},{y0}) r0: {radius0} p1: ({x1},{y1}) r1: {radius1} extend: {:?} ", extend); - print_color_stops(instance, color_stops); - } - ResolvedPaint::SweepGradient { - center_x, - center_y, - start_angle, - end_angle, - color_stops, - extend, - } => { - print!("SweepGradient center: ({center_x},{center_y}) angle: {start_angle}->{end_angle} extend: {:?} ", extend); - print_color_stops(instance, color_stops); - } - ResolvedPaint::Rotate { - angle, - around_center, - paint, - } => { - let center = around_center.unwrap_or_default(); - println!("Rotate angle: {angle} center: ({}, {})", center.x, center.y); - print_paint(instance, &paint, depth + 1); - } - ResolvedPaint::Scale { - scale_x, - scale_y, - around_center, - paint, - } => { - let center = around_center.unwrap_or_default(); - println!( - "Scale x: {scale_x} y: {scale_y} center: ({}, {})", - center.x, center.y - ); - print_paint(instance, &paint, depth + 1); - } - ResolvedPaint::Skew { - x_skew_angle, - y_skew_angle, - around_center, - paint, - } => { - let center = around_center.unwrap_or_default(); - println!( - "Skew x: {x_skew_angle} y: {y_skew_angle} center: ({}, {})", - center.x, center.y - ); - print_paint(instance, &paint, depth + 1); - } - ResolvedPaint::Solid { - palette_index, - alpha, - } => { - println!("Solid {palette_index} {alpha}"); - } - ResolvedPaint::Transform { - xx, - yx, - xy, - yy, - dx, - dy, - paint, - } => { - println!("Transform {xx} {yx} {xy} {yy} {dx} {dy}"); - print_paint(instance, &paint, depth + 1); - } - ResolvedPaint::Translate { dx, dy, paint } => { - println!("Translate x: {dx} y: {dy}"); - print_paint(instance, &paint, depth + 1); - } - } -} - -fn print_color_stops(instance: &ColrInstance, stops: ColorStops) { - print!("stops: "); - for (i, stop) in stops.resolve(instance).enumerate() { - if i > 0 { - print!(", "); - } - print!("({} {} {})", stop.offset, stop.palette_index, stop.alpha); - } - println!(); -} diff --git a/skrifa/src/scale/mod.rs b/skrifa/src/scale/mod.rs index 5c7bebcd5..0a99d57e4 100644 --- a/skrifa/src/scale/mod.rs +++ b/skrifa/src/scale/mod.rs @@ -153,8 +153,6 @@ mod error; mod glyf; mod scaler; -pub use colr::print_color_glyph; - pub use read_fonts::types::Pen; pub use error::{Error, Result}; From 484b00d378e37327df0b6d53ec03a05456ad6a37 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Tue, 14 Nov 2023 14:16:30 -0500 Subject: [PATCH 3/7] fix doc comment links --- skrifa/src/scale/colr/instance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skrifa/src/scale/colr/instance.rs b/skrifa/src/scale/colr/instance.rs index e78bd3ce5..72aa7baa0 100644 --- a/skrifa/src/scale/colr/instance.rs +++ b/skrifa/src/scale/colr/instance.rs @@ -17,7 +17,7 @@ pub type PaintId = usize; /// Combination of a `COLR` table and a location in variation space for /// resolving paints. /// -/// See [`Paint::resolve`], [`ColorStops::resolve`] and [`ClipBox::resolve`]. +/// See [`resolve_paint`], [`ColorStops::resolve`] and [`resolve_clip_box`]. pub struct ColrInstance<'a> { colr: Colr<'a>, index_map: Option>, From 8ca7838df558f261f7cc79ade6a0995754973fe5 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 15 Nov 2023 12:16:50 -0500 Subject: [PATCH 4/7] partial review feedback - renamed DeltaTy -> DeltaType - add sanity test for floating point delta computation --- read-fonts/src/tables/variations.rs | 40 ++++++++++++++++++ skrifa/src/scale/colr/instance.rs | 64 ++++++++++++++--------------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/read-fonts/src/tables/variations.rs b/read-fonts/src/tables/variations.rs index 7ed90cec6..b6a4258e4 100644 --- a/read-fonts/src/tables/variations.rs +++ b/read-fonts/src/tables/variations.rs @@ -925,4 +925,44 @@ mod tests { // in which case the iterator just keeps incrementing until u16::MAX assert_eq!(all_points.iter().count(), u16::MAX as _); } + + /// We don't have a reference for our f32 delta computation, so this is + /// a sanity test to ensure that floating point deltas are within a + /// reasonable margin of the same in fixed point. + #[test] + fn ivs_f32_deltas_nearly_match_fixed_deltas() { + let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap(); + let axis_count = font.fvar().unwrap().axis_count() as usize; + let colr = font.colr().unwrap(); + let ivs = colr.item_variation_store().unwrap().unwrap(); + // Generate a set of coords from -1 to 1 in 0.1 increments + for coord in (0..=20).map(|x| F2Dot14::from_f32((x as f32) / 10.0 - 1.0)) { + // For testing purposes, just splat the coord to all axes + let coords = vec![coord; axis_count]; + for (outer_ix, data) in ivs.item_variation_data().iter().enumerate() { + let outer_ix = outer_ix as u16; + let Some(Ok(data)) = data else { + continue; + }; + for inner_ix in 0..data.item_count() { + let delta_ix = DeltaSetIndex { + outer: outer_ix, + inner: inner_ix, + }; + let fixed_delta = ivs.compute_delta(delta_ix, &coords).unwrap(); + // FWord here is not strictly correct because this + // COLRv1 IVS contains deltas for varying types but + // it's good enough for our sanity check + let f32_delta = ivs.compute_delta_f32::(delta_ix, &coords).unwrap(); + // We need to accept both rounding and truncation + // to account for the additional accumulation of + // fractional bits in floating point + assert!( + fixed_delta == f32_delta.round() as i32 + || fixed_delta == f32_delta.trunc() as i32 + ); + } + } + } + } } diff --git a/skrifa/src/scale/colr/instance.rs b/skrifa/src/scale/colr/instance.rs index 72aa7baa0..6a750e9e4 100644 --- a/skrifa/src/scale/colr/instance.rs +++ b/skrifa/src/scale/colr/instance.rs @@ -41,7 +41,7 @@ impl<'a> ColrInstance<'a> { /// Computes a sequence of N variation deltas starting at the given /// `var_base` index. - fn var_deltas(&self, var_index_base: u32, tys: [DeltaTy; N]) -> [f32; N] { + fn var_deltas(&self, var_index_base: u32, tys: [DeltaType; N]) -> [f32; N] { // Magic value that indicates deltas should not be applied. const NO_VARIATION_DELTAS: u32 = 0xFFFFFFFF; // Note: FreeType never returns an error for these lookups, so @@ -56,12 +56,12 @@ impl<'a> ColrInstance<'a> { return deltas; } let var_store = self.var_store.as_ref().unwrap(); - let compute_delta = |ty: &DeltaTy, delta_ix| { + let compute_delta = |ty: &DeltaType, delta_ix| { match ty { - DeltaTy::FWord => var_store.compute_delta_f32::(delta_ix, self.coords), - DeltaTy::UfWord => var_store.compute_delta_f32::(delta_ix, self.coords), - DeltaTy::Fixed => var_store.compute_delta_f32::(delta_ix, self.coords), - DeltaTy::F2Dot14 => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaType::FWord => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaType::UfWord => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaType::Fixed => var_store.compute_delta_f32::(delta_ix, self.coords), + DeltaType::F2Dot14 => var_store.compute_delta_f32::(delta_ix, self.coords), } .unwrap_or_default() }; @@ -89,7 +89,7 @@ impl<'a> ColrInstance<'a> { } #[derive(Copy, Clone)] -enum DeltaTy { +enum DeltaType { FWord, UfWord, Fixed, @@ -115,7 +115,7 @@ pub fn resolve_clip_box(instance: &ColrInstance, clip_box: &ClipBox) -> Bounding y_max: cbox.y_max().to_i16() as f32, }, ClipBox::Format2(cbox) => { - let deltas = instance.var_deltas(cbox.var_index_base(), [DeltaTy::FWord; 4]); + let deltas = instance.var_deltas(cbox.var_index_base(), [DeltaType::FWord; 4]); BoundingBox { x_min: cbox.x_min().apply_delta(deltas[0]), y_min: cbox.y_min().apply_delta(deltas[1]), @@ -178,7 +178,7 @@ impl<'a> ColorStops<'a> { alpha: stop.alpha().to_f32(), }) .chain(self.var_stops.iter().map(|stop| { - let deltas = instance.var_deltas(stop.var_index_base(), [DeltaTy::F2Dot14; 2]); + let deltas = instance.var_deltas(stop.var_index_base(), [DeltaType::F2Dot14; 2]); ResolvedColorStop { offset: stop.stop_offset().apply_delta(deltas[0]), palette_index: stop.palette_index(), @@ -302,7 +302,7 @@ pub fn resolve_paint<'a>( alpha: solid.alpha().to_f32(), }, Paint::VarSolid(solid) => { - let deltas = instance.var_deltas(solid.var_index_base(), [DeltaTy::F2Dot14]); + let deltas = instance.var_deltas(solid.var_index_base(), [DeltaType::F2Dot14]); ResolvedPaint::Solid { palette_index: solid.palette_index(), alpha: solid.alpha().apply_delta(deltas[0]), @@ -325,7 +325,7 @@ pub fn resolve_paint<'a>( Paint::VarLinearGradient(gradient) => { let color_line = gradient.color_line()?; let extend = color_line.extend(); - let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaTy::FWord; 6]); + let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaType::FWord; 6]); ResolvedPaint::LinearGradient { x0: gradient.x0().apply_delta(deltas[0]), y0: gradient.y0().apply_delta(deltas[1]), @@ -354,7 +354,7 @@ pub fn resolve_paint<'a>( Paint::VarRadialGradient(gradient) => { let color_line = gradient.color_line()?; let extend = color_line.extend(); - let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaTy::FWord; 6]); + let deltas = instance.var_deltas(gradient.var_index_base(), [DeltaType::FWord; 6]); ResolvedPaint::RadialGradient { x0: gradient.x0().apply_delta(deltas[0]), y0: gradient.y0().apply_delta(deltas[1]), @@ -384,10 +384,10 @@ pub fn resolve_paint<'a>( let deltas = instance.var_deltas( gradient.var_index_base(), [ - DeltaTy::FWord, - DeltaTy::FWord, - DeltaTy::F2Dot14, - DeltaTy::F2Dot14, + DeltaType::FWord, + DeltaType::FWord, + DeltaType::F2Dot14, + DeltaType::F2Dot14, ], ); ResolvedPaint::SweepGradient { @@ -422,7 +422,7 @@ pub fn resolve_paint<'a>( Paint::VarTransform(transform) => { let affine = transform.transform()?; let paint = transform.paint()?; - let deltas = instance.var_deltas(affine.var_index_base(), [DeltaTy::Fixed; 6]); + let deltas = instance.var_deltas(affine.var_index_base(), [DeltaType::Fixed; 6]); ResolvedPaint::Transform { xx: affine.xx().apply_delta(deltas[0]), yx: affine.yx().apply_delta(deltas[1]), @@ -439,7 +439,7 @@ pub fn resolve_paint<'a>( paint: transform.paint()?, }, Paint::VarTranslate(transform) => { - let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::FWord; 2]); + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaType::FWord; 2]); ResolvedPaint::Translate { dx: transform.dx().apply_delta(deltas[0]), dy: transform.dy().apply_delta(deltas[1]), @@ -453,7 +453,7 @@ pub fn resolve_paint<'a>( paint: transform.paint()?, }, Paint::VarScale(transform) => { - let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14; 2]); + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaType::F2Dot14; 2]); ResolvedPaint::Scale { scale_x: transform.scale_x().apply_delta(deltas[0]), scale_y: transform.scale_y().apply_delta(deltas[1]), @@ -474,10 +474,10 @@ pub fn resolve_paint<'a>( let deltas = instance.var_deltas( transform.var_index_base(), [ - DeltaTy::F2Dot14, - DeltaTy::F2Dot14, - DeltaTy::FWord, - DeltaTy::FWord, + DeltaType::F2Dot14, + DeltaType::F2Dot14, + DeltaType::FWord, + DeltaType::FWord, ], ); ResolvedPaint::Scale { @@ -500,7 +500,7 @@ pub fn resolve_paint<'a>( } } Paint::VarScaleUniform(transform) => { - let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14]); + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaType::F2Dot14]); let scale = transform.scale().apply_delta(deltas[0]); ResolvedPaint::Scale { scale_x: scale, @@ -524,7 +524,7 @@ pub fn resolve_paint<'a>( Paint::VarScaleUniformAroundCenter(transform) => { let deltas = instance.var_deltas( transform.var_index_base(), - [DeltaTy::F2Dot14, DeltaTy::FWord, DeltaTy::FWord], + [DeltaType::F2Dot14, DeltaType::FWord, DeltaType::FWord], ); let scale = transform.scale().apply_delta(deltas[0]); ResolvedPaint::Scale { @@ -543,7 +543,7 @@ pub fn resolve_paint<'a>( paint: transform.paint()?, }, Paint::VarRotate(transform) => { - let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14]); + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaType::F2Dot14]); ResolvedPaint::Rotate { angle: transform.angle().apply_delta(deltas[0]), around_center: None, @@ -561,7 +561,7 @@ pub fn resolve_paint<'a>( Paint::VarRotateAroundCenter(transform) => { let deltas = instance.var_deltas( transform.var_index_base(), - [DeltaTy::F2Dot14, DeltaTy::FWord, DeltaTy::FWord], + [DeltaType::F2Dot14, DeltaType::FWord, DeltaType::FWord], ); ResolvedPaint::Rotate { angle: transform.angle().apply_delta(deltas[0]), @@ -579,7 +579,7 @@ pub fn resolve_paint<'a>( paint: transform.paint()?, }, Paint::VarSkew(transform) => { - let deltas = instance.var_deltas(transform.var_index_base(), [DeltaTy::F2Dot14; 2]); + let deltas = instance.var_deltas(transform.var_index_base(), [DeltaType::F2Dot14; 2]); ResolvedPaint::Skew { x_skew_angle: transform.x_skew_angle().apply_delta(deltas[0]), y_skew_angle: transform.y_skew_angle().apply_delta(deltas[1]), @@ -600,10 +600,10 @@ pub fn resolve_paint<'a>( let deltas = instance.var_deltas( transform.var_index_base(), [ - DeltaTy::F2Dot14, - DeltaTy::F2Dot14, - DeltaTy::FWord, - DeltaTy::FWord, + DeltaType::F2Dot14, + DeltaType::F2Dot14, + DeltaType::FWord, + DeltaType::FWord, ], ); ResolvedPaint::Skew { From 9671285a45ac2eda13d9d9473261730f1738bcf1 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Wed, 15 Nov 2023 13:04:51 -0500 Subject: [PATCH 5/7] [skrifa] public color API Expose a color outline API in top level metadata rather than in the scale module. --- skrifa/src/{scale/colr => color}/instance.rs | 1 + skrifa/src/color/mod.rs | 127 +++++++++++++++++++ skrifa/src/lib.rs | 2 + skrifa/src/provider.rs | 6 + skrifa/src/scale/colr/mod.rs | 6 - skrifa/src/scale/mod.rs | 1 - 6 files changed, 136 insertions(+), 7 deletions(-) rename skrifa/src/{scale/colr => color}/instance.rs (99%) create mode 100644 skrifa/src/color/mod.rs delete mode 100644 skrifa/src/scale/colr/mod.rs diff --git a/skrifa/src/scale/colr/instance.rs b/skrifa/src/color/instance.rs similarity index 99% rename from skrifa/src/scale/colr/instance.rs rename to skrifa/src/color/instance.rs index 6a750e9e4..f68fb1520 100644 --- a/skrifa/src/scale/colr/instance.rs +++ b/skrifa/src/color/instance.rs @@ -18,6 +18,7 @@ pub type PaintId = usize; /// resolving paints. /// /// See [`resolve_paint`], [`ColorStops::resolve`] and [`resolve_clip_box`]. +#[derive(Clone)] pub struct ColrInstance<'a> { colr: Colr<'a>, index_map: Option>, diff --git a/skrifa/src/color/mod.rs b/skrifa/src/color/mod.rs new file mode 100644 index 000000000..09effb672 --- /dev/null +++ b/skrifa/src/color/mod.rs @@ -0,0 +1,127 @@ +//! Color outline support. + +// Remove me when code is filled in +#![allow(unused_variables, dead_code)] + +mod instance; + +use core::ops::Range; + +use crate::{instance::LocationRef, metrics::BoundingBox}; +use read_fonts::{tables::colr, types::GlyphId, ReadError, TableProvider}; + +use instance::{resolve_paint, PaintId}; + +/// Interface for receiving a sequence of commands that represent a flattened +/// paint graph in a color outline. +// Placeholder +pub trait ColorPainter {} + +/// Affine transformation matrix. +#[derive(Copy, Clone, Debug)] +pub struct Transform { + xx: f32, + yx: f32, + xy: f32, + yy: f32, + dx: f32, + dy: f32, +} + +/// Reference to a paint graph that represents a color glyph outline. +#[derive(Clone)] +pub struct ColorOutline<'a> { + colr: colr::Colr<'a>, + glyph_id: GlyphId, + kind: ColorOutlineKind<'a>, +} + +impl<'a> ColorOutline<'a> { + /// Returns the glyph identifier that was used to retrieve this outline. + pub fn glyph_id(&self) -> GlyphId { + self.glyph_id + } + + /// Evaluates the paint graph at the specified location in variation space + /// and returns the bounding box for the full scene. + /// + /// The `glyph_bounds` closure will be invoked for each clip node in the + /// graph and should return the bounding box for the given glyph, location + /// and transform. + pub fn bounding_box( + &self, + location: impl Into>, + glyph_bounds: impl FnMut(GlyphId, &LocationRef, Transform) -> Option, + ) -> Option { + None + } + + /// Evaluates the paint graph at the specified location in variation space + /// and emits the results to the given painter. + pub fn paint( + &self, + location: impl Into>, + painter: &mut impl ColorPainter, + ) -> Result<(), ReadError> { + let instance = instance::ColrInstance::new(self.colr.clone(), location.into().coords()); + match &self.kind { + ColorOutlineKind::V0(layer_range) => { + for layer_ix in layer_range.clone() { + let (glyph_id, palette_ix) = instance.v0_layer(layer_ix)?; + } + } + ColorOutlineKind::V1(paint, paint_id) => { + let paint = resolve_paint(&instance, paint)?; + } + } + Ok(()) + } +} + +#[derive(Clone)] +enum ColorOutlineKind<'a> { + V0(Range), + V1(colr::Paint<'a>, PaintId), +} + +/// Collection of color outlines. +#[derive(Clone)] +pub struct ColorOutlineCollection<'a> { + glyph_count: u16, + colr: Option>, +} + +impl<'a> ColorOutlineCollection<'a> { + /// Creates a new color outline collection for the given font. + pub fn new(font: &impl TableProvider<'a>) -> Self { + let glyph_count = font + .maxp() + .map(|maxp| maxp.num_glyphs()) + .unwrap_or_default(); + let colr = font.colr().ok(); + Self { glyph_count, colr } + } + + /// Returns the color outline for the given glyph identifier. + pub fn get(&self, glyph_id: GlyphId) -> Option> { + let colr = self.colr.clone()?; + let kind = if let Ok(Some((paint, paint_id))) = colr.v1_base_glyph(glyph_id) { + ColorOutlineKind::V1(paint, paint_id) + } else { + let layer_range = colr.v0_base_glyph(glyph_id).ok()??; + ColorOutlineKind::V0(layer_range) + }; + Some(ColorOutline { + colr, + glyph_id, + kind, + }) + } + + /// Returns an iterator over all of the color outlines in the + /// collection. + pub fn iter(&self) -> impl Iterator> + 'a + Clone { + let copy = self.clone(); + (0..self.glyph_count).filter_map(move |gid| copy.get(GlyphId::new(gid))) + } +} diff --git a/skrifa/src/lib.rs b/skrifa/src/lib.rs index 6cb682194..765ae913d 100644 --- a/skrifa/src/lib.rs +++ b/skrifa/src/lib.rs @@ -24,10 +24,12 @@ pub mod scale; pub mod setting; pub mod string; +mod color; mod provider; mod small_array; mod variation; +pub use color::{ColorOutline, ColorOutlineCollection, ColorPainter, Transform}; pub use variation::{Axis, AxisCollection, NamedInstance, NamedInstanceCollection}; /// Useful collection of common types suitable for glob importing. diff --git a/skrifa/src/provider.rs b/skrifa/src/provider.rs index 9c699d1ce..2c8ab38f1 100644 --- a/skrifa/src/provider.rs +++ b/skrifa/src/provider.rs @@ -1,6 +1,7 @@ use super::{ attribute::Attributes, charmap::Charmap, + color::ColorOutlineCollection, instance::{LocationRef, Size}, metrics::{GlyphMetrics, Metrics}, string::{LocalizedStrings, StringId}, @@ -47,6 +48,11 @@ pub trait MetadataProvider<'a>: raw::TableProvider<'a> + Sized { fn charmap(&self) -> Charmap<'a> { Charmap::new(self) } + + /// Returns the collection of color outlines. + fn color_outlines(&self) -> ColorOutlineCollection<'a> { + ColorOutlineCollection::new(self) + } } /// Blanket implementation of `MetadataProvider` for any type that implements diff --git a/skrifa/src/scale/colr/mod.rs b/skrifa/src/scale/colr/mod.rs deleted file mode 100644 index b92097a37..000000000 --- a/skrifa/src/scale/colr/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod instance; - -pub use instance::{ - resolve_clip_box, resolve_paint, ColorStops, ColrInstance, PaintId, ResolvedColorStop, - ResolvedPaint, -}; diff --git a/skrifa/src/scale/mod.rs b/skrifa/src/scale/mod.rs index affd0ca5c..77e3bda68 100644 --- a/skrifa/src/scale/mod.rs +++ b/skrifa/src/scale/mod.rs @@ -148,7 +148,6 @@ #![allow(dead_code)] mod cff; -mod colr; mod error; mod glyf; mod scaler; From fdf5a134830f288bc9a5a884f6beedfaff7505ff Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 16 Nov 2023 11:30:35 -0500 Subject: [PATCH 6/7] review feedback - add version() method to ColorOutline - remove maxp lookup and change iterator to use max glyph in COLR table - remove glyph_id from ColorOutline and change iterator to return (GlyphId, ColorOutline) tuple --- skrifa/src/color/mod.rs | 65 ++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/skrifa/src/color/mod.rs b/skrifa/src/color/mod.rs index 09effb672..aa5e1dfc7 100644 --- a/skrifa/src/color/mod.rs +++ b/skrifa/src/color/mod.rs @@ -20,26 +20,29 @@ pub trait ColorPainter {} /// Affine transformation matrix. #[derive(Copy, Clone, Debug)] pub struct Transform { - xx: f32, - yx: f32, - xy: f32, - yy: f32, - dx: f32, - dy: f32, + pub xx: f32, + pub yx: f32, + pub xy: f32, + pub yy: f32, + pub dx: f32, + pub dy: f32, } /// Reference to a paint graph that represents a color glyph outline. #[derive(Clone)] pub struct ColorOutline<'a> { colr: colr::Colr<'a>, - glyph_id: GlyphId, kind: ColorOutlineKind<'a>, } impl<'a> ColorOutline<'a> { - /// Returns the glyph identifier that was used to retrieve this outline. - pub fn glyph_id(&self) -> GlyphId { - self.glyph_id + /// Returns the version of the color table from which this outline was + /// selected. + pub fn version(&self) -> u32 { + match &self.kind { + ColorOutlineKind::V0(..) => 0, + ColorOutlineKind::V1(..) => 1, + } } /// Evaluates the paint graph at the specified location in variation space @@ -87,19 +90,14 @@ enum ColorOutlineKind<'a> { /// Collection of color outlines. #[derive(Clone)] pub struct ColorOutlineCollection<'a> { - glyph_count: u16, colr: Option>, } impl<'a> ColorOutlineCollection<'a> { /// Creates a new color outline collection for the given font. pub fn new(font: &impl TableProvider<'a>) -> Self { - let glyph_count = font - .maxp() - .map(|maxp| maxp.num_glyphs()) - .unwrap_or_default(); let colr = font.colr().ok(); - Self { glyph_count, colr } + Self { colr } } /// Returns the color outline for the given glyph identifier. @@ -111,17 +109,38 @@ impl<'a> ColorOutlineCollection<'a> { let layer_range = colr.v0_base_glyph(glyph_id).ok()??; ColorOutlineKind::V0(layer_range) }; - Some(ColorOutline { - colr, - glyph_id, - kind, - }) + Some(ColorOutline { colr, kind }) } /// Returns an iterator over all of the color outlines in the /// collection. - pub fn iter(&self) -> impl Iterator> + 'a + Clone { + pub fn iter(&self) -> impl Iterator)> + 'a + Clone { let copy = self.clone(); - (0..self.glyph_count).filter_map(move |gid| copy.get(GlyphId::new(gid))) + let max_glyph = copy + .colr + .as_ref() + .map(|colr| { + let max_v0 = if let Some(Ok(recs)) = colr.base_glyph_records() { + recs.last() + .map(|rec| rec.glyph_id().to_u16()) + .unwrap_or_default() + } else { + 0 + }; + let max_v1 = if let Some(Ok(list)) = colr.base_glyph_list() { + list.base_glyph_paint_records() + .last() + .map(|rec| rec.glyph_id().to_u16()) + .unwrap_or_default() + } else { + 0 + }; + max_v0.max(max_v1) + }) + .unwrap_or_default(); + (0..=max_glyph).filter_map(move |gid| { + let gid = GlyphId::new(gid); + copy.get(gid).map(|outline| (gid, outline)) + }) } } From 2c9308a653714f80ddc890c95a5345c6f1b36a85 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Thu, 16 Nov 2023 11:39:02 -0500 Subject: [PATCH 7/7] add test for iterator and version --- skrifa/src/color/mod.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/skrifa/src/color/mod.rs b/skrifa/src/color/mod.rs index aa5e1dfc7..383a9a905 100644 --- a/skrifa/src/color/mod.rs +++ b/skrifa/src/color/mod.rs @@ -144,3 +144,25 @@ impl<'a> ColorOutlineCollection<'a> { }) } } + +#[cfg(test)] +mod tests { + use crate::MetadataProvider; + use read_fonts::{types::GlyphId, FontRef}; + + #[test] + fn colr_outline_iter_and_version() { + let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap(); + let outlines = font.color_outlines(); + // This font contains one COLRv0 glyph: + // + let colrv0_outlines = [GlyphId::new(166)]; + for (gid, outline) in outlines.iter() { + let expected_version = if colrv0_outlines.contains(&gid) { 0 } else { 1 }; + assert_eq!(outline.version(), expected_version); + } + // + // + assert_eq!(outlines.iter().count(), 158); + } +}