diff --git a/sparse_strips/vello_common/src/glyph.rs b/sparse_strips/vello_common/src/glyph.rs index a7a0665e8..766c2aaac 100644 --- a/sparse_strips/vello_common/src/glyph.rs +++ b/sparse_strips/vello_common/src/glyph.rs @@ -4,6 +4,7 @@ //! Processing and drawing glyphs. use crate::peniko::Font; +use skrifa::OutlineGlyphCollection; use skrifa::instance::Size; use skrifa::outline::DrawSettings; use skrifa::{ @@ -27,8 +28,8 @@ pub enum PreparedGlyph<'a> { pub struct OutlineGlyph<'a> { /// The path of the glyph. pub path: &'a BezPath, - /// The local transform of the glyph. - pub local_transform: Affine, + /// The global transform of the glyph. + pub transform: Affine, } /// Trait for types that can render glyphs. @@ -56,6 +57,7 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { font_size: 16.0, transform, glyph_transform: None, + horizontal_skew: None, hint: true, normalized_coords: &[], }, @@ -69,12 +71,18 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { self } - /// Set the per-glyph transform. Can be used to apply skew to simulate italic text. + /// Set the per-glyph transform. Use `horizontal_skew` to simulate italic text. pub fn glyph_transform(mut self, transform: Affine) -> Self { self.run.glyph_transform = Some(transform); self } + /// Set the horizontal skew angle in radians to simulate italic/oblique text. + pub fn horizontal_skew(mut self, angle: f32) -> Self { + self.run.horizontal_skew = Some(angle); + self + } + /// Set whether font hinting is enabled. pub fn hint(mut self, hint: bool) -> Self { self.run.hint = hint; @@ -101,22 +109,16 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { let font = skrifa::FontRef::from_index(self.run.font.data.as_ref(), self.run.font.index).unwrap(); let outlines = font.outline_glyphs(); - let size = Size::new(self.run.font_size); - let hinting_instance = if self.run.hint { - // Only apply hinting if the transform is a simple translation. - // Scaled, rotated, skewed, and other transformations cannot be hinted. - let [a, b, c, d, _, _] = self.run.transform.as_coeffs(); - // TODO: Consider scaling the font size if the transform is a uniform scale. - if a == 1.0 && d == 1.0 && b == 0.0 && c == 0.0 { - // TODO: Cache hinting instance. - HintingInstance::new(&outlines, size, self.run.normalized_coords, HINTING_OPTIONS) - .ok() - } else { - None - } - } else { - None - }; + + let PreparedGlyphRun { + transform, + glyph_transform, + size, + scale, + horizontal_skew, + normalized_coords, + hinting_instance, + } = prepare_glyph_run(&self.run, &outlines); let render_glyph = match style { Style::Fill => GlyphRenderer::fill_glyph, @@ -128,7 +130,7 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { let draw_settings = if let Some(hinting_instance) = &hinting_instance { DrawSettings::hinted(hinting_instance, false) } else { - DrawSettings::unhinted(size, self.run.normalized_coords) + DrawSettings::unhinted(size, normalized_coords) }; let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else { continue; @@ -137,16 +139,21 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { if outline.draw(draw_settings, &mut path).is_err() { continue; } - let mut transform = Affine::translate(Vec2::new(glyph.x as f64, glyph.y as f64)); - if let Some(glyph_transform) = self.run.glyph_transform { - transform *= glyph_transform; + + let mut local_transform = + Affine::translate(Vec2::new(glyph.x as f64 * scale, glyph.y as f64 * scale)); + if let Some(skew) = horizontal_skew { + local_transform *= Affine::skew(skew.tan() as f64, 0.0); + } + if let Some(glyph_transform) = glyph_transform { + local_transform *= glyph_transform; } render_glyph( self.renderer, PreparedGlyph::Outline(OutlineGlyph { path: &path.0, - local_transform: transform, + transform: transform * local_transform, }), ); } @@ -167,14 +174,97 @@ struct GlyphRun<'a> { font_size: f32, /// Global transform. transform: Affine, - /// Per-glyph transform. Can be used to apply skew to simulate italic text. + /// Per-glyph transform. Use `horizontal_skew` to simulate italic text. glyph_transform: Option, + /// Horizontal skew angle in radians for simulating italic/oblique text. + horizontal_skew: Option, /// Normalized variation coordinates for variable fonts. normalized_coords: &'a [skrifa::instance::NormalizedCoord], /// Controls whether font hinting is enabled. hint: bool, } +struct PreparedGlyphRun<'a> { + transform: Affine, + glyph_transform: Option, + size: Size, + scale: f64, + horizontal_skew: Option, + normalized_coords: &'a [skrifa::instance::NormalizedCoord], + hinting_instance: Option, +} + +/// Prepare a glyph run for rendering. +/// +/// This function calculates the appropriate transform, size, and scaling parameters +/// for proper font hinting when enabled and possible. +fn prepare_glyph_run<'a>( + run: &GlyphRun<'a>, + outlines: &OutlineGlyphCollection<'_>, +) -> PreparedGlyphRun<'a> { + // TODO: Consider extracting the scale from the glyph transform and applying it to the font size. + if !run.hint || run.glyph_transform.is_some() { + return PreparedGlyphRun { + transform: run.transform, + glyph_transform: run.glyph_transform, + size: Size::new(run.font_size), + scale: 1.0, + horizontal_skew: run.horizontal_skew, + normalized_coords: run.normalized_coords, + hinting_instance: None, + }; + } + + // Hinting doesn't make sense if we later scale the glyphs via some transform. So, if + // this glyph can be scaled uniformly, we extract the scale from its global and glyph + // transform and apply it to font size for hinting. Note that this extracted scale + // should be later applied to the glyph's position. + // + // If the glyph is rotated or skewed, hinting is not applicable. + + // Attempt to extract uniform scale from the run's transform. + if let Some((scale, transform)) = take_uniform_scale(run.transform) { + let font_size = run.font_size * scale as f32; + + let size = Size::new(font_size); + let hinting_instance = + HintingInstance::new(outlines, size, run.normalized_coords, HINTING_OPTIONS).ok(); + + return PreparedGlyphRun { + transform, + glyph_transform: run.glyph_transform, + size, + scale, + horizontal_skew: run.horizontal_skew, + normalized_coords: run.normalized_coords, + hinting_instance, + }; + } + + PreparedGlyphRun { + transform: run.transform, + glyph_transform: run.glyph_transform, + size: Size::new(run.font_size), + scale: 1.0, + horizontal_skew: run.horizontal_skew, + normalized_coords: run.normalized_coords, + hinting_instance: None, + } +} + +/// If `transform` has a uniform scale without rotation or skew, return the scale factor and the +/// transform with the scale factored out. Translation is unchanged. +fn take_uniform_scale(transform: Affine) -> Option<(f64, Affine)> { + let [a, b, c, d, e, f] = transform.as_coeffs(); + if a == d && b == 0.0 && c == 0.0 { + let extracted_scale = a; + let transform_without_scale = Affine::new([1.0, 0.0, 0.0, 1.0, e, f]); + Some((extracted_scale, transform_without_scale)) + } else { + None + } +} + // TODO: Although these are sane defaults, we might want to make them // configurable. const HINTING_OPTIONS: HintingOptions = HintingOptions { @@ -233,4 +323,75 @@ mod tests { const _NORMALISED_COORD_SIZE_MATCHES: () = assert!(size_of::() == size_of::()); + + mod take_uniform_scale { + use super::*; + + #[test] + fn identity_transform() { + let identity = Affine::IDENTITY; + let result = take_uniform_scale(identity); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 1.0).abs() < 1e-10); + assert_eq!(transform, Affine::IDENTITY); + } + + #[test] + fn pure_uniform_scale() { + let scale_transform = Affine::scale(2.5); + let result = take_uniform_scale(scale_transform); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 2.5).abs() < 1e-10); + assert_eq!(transform, Affine::IDENTITY); + } + + #[test] + fn scale_with_translation() { + let scale_translate = Affine::scale(3.0).then_translate(Vec2::new(10.0, 20.0)); + let result = take_uniform_scale(scale_translate); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 3.0).abs() < 1e-10); + // The translation should be adjusted by the scale factor + assert_eq!(transform, Affine::translate(Vec2::new(10.0, 20.0))); + } + + #[test] + fn pure_translation() { + let translation = Affine::translate(Vec2::new(5.0, 7.0)); + let result = take_uniform_scale(translation); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 1.0).abs() < 1e-10); + assert_eq!(transform, translation); + } + + #[test] + fn non_uniform_scale() { + let non_uniform = Affine::scale_non_uniform(2.0, 3.0); + assert!(take_uniform_scale(non_uniform).is_none()); + } + + #[test] + fn rotation_transform() { + let rotation = Affine::rotate(std::f64::consts::PI / 4.0); + assert!(take_uniform_scale(rotation).is_none()); + } + + #[test] + fn skew_transform() { + let skew = Affine::skew(0.5, 0.0); + assert!(take_uniform_scale(skew).is_none()); + } + + #[test] + fn complex_transform() { + let complex = Affine::translate(Vec2::new(10.0, 20.0)) + .then_rotate(std::f64::consts::PI / 6.0) + .then_scale(2.0); + assert!(take_uniform_scale(complex).is_none()); + } + } } diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_filled_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_filled_unhinted.png new file mode 100644 index 000000000..983a5520d --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_filled_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f0f044f7f7cd21a94bbf2dc8a574d485e75ef0d63f3f7eaf0d784ddd6c711de +size 2538 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png new file mode 100644 index 000000000..3ce1b5500 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eafa457ec56c740a448498b4949b96445e78be6d2bf6d54052a21f318f87f306 +size 2329 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform_unhinted.png new file mode 100644 index 000000000..3ce1b5500 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eafa457ec56c740a448498b4949b96445e78be6d2bf6d54052a21f318f87f306 +size 2329 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_scaled.png b/sparse_strips/vello_cpu/snapshots/glyphs_scaled.png index dd68a3618..6e3611855 100644 --- a/sparse_strips/vello_cpu/snapshots/glyphs_scaled.png +++ b/sparse_strips/vello_cpu/snapshots/glyphs_scaled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81197e38ec9d32f7fd6a1124fa3cb28d0e2f6ed03f70eb4f163e1864db31dd5f -size 2527 +oid sha256:5305569218595bbe61303802b495c4a2104b96daf228d9fd3cb2ac6c9d9a5fe5 +size 2434 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_scaled_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_scaled_unhinted.png new file mode 100644 index 000000000..dd68a3618 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_scaled_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81197e38ec9d32f7fd6a1124fa3cb28d0e2f6ed03f70eb4f163e1864db31dd5f +size 2527 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unhinted.png new file mode 100644 index 000000000..a95929b87 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1284ce5f8bb9622d5beec15c6f298fe676c83f5c7e28a41035ab031470bc7f8a +size 3283 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_stroked_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_stroked_unhinted.png new file mode 100644 index 000000000..7af42fab9 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_stroked_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02c26064d4e9c296ba94f08de87e4106edf3dae9cf4f17b124a2b44aa7fea4db +size 3415 diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 5ed8261c8..7f5989200 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -221,8 +221,7 @@ impl GlyphRenderer for RenderContext { fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>) { match glyph { PreparedGlyph::Outline(glyph) => { - let transform = self.transform * glyph.local_transform; - flatten::fill(glyph.path, transform, &mut self.line_buf); + flatten::fill(glyph.path, glyph.transform, &mut self.line_buf); self.render_path(Fill::NonZero, self.paint.clone()); } } @@ -231,8 +230,12 @@ impl GlyphRenderer for RenderContext { fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>) { match glyph { PreparedGlyph::Outline(glyph) => { - let transform = self.transform * glyph.local_transform; - flatten::stroke(glyph.path, &self.stroke, transform, &mut self.line_buf); + flatten::stroke( + glyph.path, + &self.stroke, + glyph.transform, + &mut self.line_buf, + ); self.render_path(Fill::NonZero, self.paint.clone()); } } diff --git a/sparse_strips/vello_cpu/tests/glyph.rs b/sparse_strips/vello_cpu/tests/glyph.rs index 056c4e532..e024d1b9f 100644 --- a/sparse_strips/vello_cpu/tests/glyph.rs +++ b/sparse_strips/vello_cpu/tests/glyph.rs @@ -17,11 +17,28 @@ fn glyphs_filled() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) + .hint(true) .fill_glyphs(glyphs.into_iter()); check_ref(&ctx, "glyphs_filled"); } +#[test] +fn glyphs_filled_unhinted() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_filled_unhinted"); +} + #[test] fn glyphs_stroked() { let mut ctx = get_ctx(300, 70, false); @@ -32,11 +49,28 @@ fn glyphs_stroked() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) + .hint(true) .stroke_glyphs(glyphs.into_iter()); check_ref(&ctx, "glyphs_stroked"); } +#[test] +fn glyphs_stroked_unhinted() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .hint(false) + .stroke_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_stroked_unhinted"); +} + #[test] fn glyphs_skewed() { let mut ctx = get_ctx(300, 70, false); @@ -47,12 +81,30 @@ fn glyphs_skewed() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) - .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.0)) + .horizontal_skew(-20_f32.to_radians()) + .hint(true) .fill_glyphs(glyphs.into_iter()); check_ref(&ctx, "glyphs_skewed"); } +#[test] +fn glyphs_skewed_unhinted() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .horizontal_skew(-20_f32.to_radians()) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_skewed_unhinted"); +} + #[test] fn glyphs_scaled() { let mut ctx = get_ctx(150, 125, false); @@ -63,7 +115,58 @@ fn glyphs_scaled() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) + .hint(true) .fill_glyphs(glyphs.into_iter()); check_ref(&ctx, "glyphs_scaled"); } + +#[test] +fn glyphs_scaled_unhinted() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 25_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0)); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_scaled_unhinted"); +} + +#[test] +fn glyphs_glyph_transform() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 25_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0)); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::translate((10., 10.))) + .hint(true) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_glyph_transform"); +} + +#[test] +fn glyphs_glyph_transform_unhinted() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 25_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0)); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::translate((10., 10.))) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_glyph_transform_unhinted"); +} diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index a095acb2e..317c6fc86 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -274,8 +274,7 @@ impl GlyphRenderer for Scene { fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>) { match glyph { PreparedGlyph::Outline(glyph) => { - let transform = self.transform * glyph.local_transform; - flatten::fill(glyph.path, transform, &mut self.line_buf); + flatten::fill(glyph.path, glyph.transform, &mut self.line_buf); self.render_path(Fill::NonZero, self.paint.clone()); } } @@ -284,8 +283,12 @@ impl GlyphRenderer for Scene { fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>) { match glyph { PreparedGlyph::Outline(glyph) => { - let transform = self.transform * glyph.local_transform; - flatten::stroke(glyph.path, &self.stroke, transform, &mut self.line_buf); + flatten::stroke( + glyph.path, + &self.stroke, + glyph.transform, + &mut self.line_buf, + ); self.render_path(Fill::NonZero, self.paint.clone()); } }