diff --git a/sparse_strips/vello_common/src/glyph.rs b/sparse_strips/vello_common/src/glyph.rs index aaeef26ec..1c85fa366 100644 --- a/sparse_strips/vello_common/src/glyph.rs +++ b/sparse_strips/vello_common/src/glyph.rs @@ -57,7 +57,6 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { font_size: 16.0, transform, glyph_transform: None, - horizontal_skew: None, hint: true, normalized_coords: &[], }, @@ -71,19 +70,17 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { self } - /// Set the per-glyph transform. Use `horizontal_skew` to simulate italic text. + /// Set the per-glyph transform. Use `Affine::skew` with a horizontal-only 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. + /// + /// This performs vertical hinting only. Hinting is performed only if the combined `transform` + /// and `glyph_transform` have a uniform scale and no vertical skew or rotation. pub fn hint(mut self, hint: bool) -> Self { self.run.hint = hint; self @@ -112,10 +109,7 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { let PreparedGlyphRun { transform, - glyph_transform, size, - scale, - horizontal_skew, normalized_coords, hinting_instance, } = prepare_glyph_run(&self.run, &outlines); @@ -140,18 +134,21 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { continue; } - 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; - } + // Calculate the global glyph translation based on the glyph's local position within + // the run and the run's global transform. + // + // This is a partial affine matrix multiplication, calculating only the translation + // component that we need. It is added below to calculate the total transform of this + // glyph. + let [a, b, c, d, _, _] = self.run.transform.as_coeffs(); + let translation = Vec2::new( + a * glyph.x as f64 + c * glyph.y as f64, + b * glyph.x as f64 + d * glyph.y as f64, + ); // When hinting, ensure the y-offset is integer. The x-offset doesn't matter, as we // perform vertical-only hinting. - let mut total_transform = (transform * local_transform).as_coeffs(); + let mut total_transform = transform.then_translate(translation).as_coeffs(); if hinting_instance.is_some() { total_transform[5] = total_transform[5].round(); } @@ -181,10 +178,9 @@ struct GlyphRun<'a> { font_size: f32, /// Global transform. transform: Affine, - /// Per-glyph transform. Use `horizontal_skew` to simulate italic text. + /// Per-glyph transform. Use [`Affine::skew`] with horizontal-skew only 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. @@ -192,11 +188,11 @@ struct GlyphRun<'a> { } struct PreparedGlyphRun<'a> { + /// The total transform (`global_transform * glyph_transform`), not accounting for glyph + /// translation. transform: Affine, - glyph_transform: Option, + /// The font size to generate glyph outlines for. size: Size, - scale: f64, - horizontal_skew: Option, normalized_coords: &'a [skrifa::instance::NormalizedCoord], hinting_instance: Option, } @@ -209,66 +205,50 @@ 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() { + if !run.hint { return PreparedGlyphRun { - transform: run.transform, - glyph_transform: run.glyph_transform, + transform: run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY), 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. + // We perform vertical-only hinting. + // + // Hinting doesn't make sense if we later scale the glyphs via some transform. So we extract + // the scale from the global transform and glyph transform and apply it to the font size for + // hinting. We do require the scaling to be uniform: simply using the vertical scale as font + // size and then transforming by the relative horizontal scale can cause, e.g., overlapping + // glyphs. 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. + // As the hinting is vertical-only, we can handle horizontal skew, but not vertical skew or + // rotations. - // 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 total_transform = run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY); + let [t_a, t_b, t_c, t_d, t_e, t_f] = total_transform.as_coeffs(); - let size = Size::new(font_size); + let uniform_scale = t_a == t_d; + let vertically_uniform = t_b == 0.; + + if uniform_scale && vertically_uniform { + let vertical_font_size = run.font_size * t_d as f32; + let size = Size::new(vertical_font_size); let hinting_instance = HintingInstance::new(outlines, size, run.normalized_coords, HINTING_OPTIONS).ok(); - - return PreparedGlyphRun { - transform, - glyph_transform: run.glyph_transform, + PreparedGlyphRun { + transform: Affine::new([1., 0., t_c, 1., t_e, t_f]), 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 + PreparedGlyphRun { + transform: run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY), + size: Size::new(run.font_size), + normalized_coords: run.normalized_coords, + hinting_instance: None, + } } } @@ -330,75 +310,4 @@ 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_glyph_transform.png b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png index 3ce1b5500..b3c908f6f 100644 --- a/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png +++ b/sparse_strips/vello_cpu/snapshots/glyphs_glyph_transform.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eafa457ec56c740a448498b4949b96445e78be6d2bf6d54052a21f318f87f306 -size 2329 +oid sha256:57366c7292c5baa6f284e210ecf96caa8ad824322284560019ac6bd2608b211d +size 2193 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long.png b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long.png new file mode 100644 index 000000000..b3c5b25df --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8b1e407fd2e43b3954bbb4dd3e2b956327bdcc8f4ff0c7b0559b8453a936246 +size 5447 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long_unhinted.png new file mode 100644 index 000000000..a703751b5 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_long_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9f09a8159a0cea0fd5029245547001b6b3bc3d01b09d9d2971a352d75052d15 +size 5838 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed.png b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed.png new file mode 100644 index 000000000..06b534452 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2122d66c7ace30548cf1b6e9a4e9ead05984fa88e1650db996e3f0bbdec6c73b +size 2279 diff --git a/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed_unhinted.png b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed_unhinted.png new file mode 100644 index 000000000..880caaf7c --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/glyphs_skewed_unskewed_unhinted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d3cb5bf9c8c938477bb7ef3d96a011d0c00a554d051474e4d37ebf5f05233ab +size 2439 diff --git a/sparse_strips/vello_cpu/tests/glyph.rs b/sparse_strips/vello_cpu/tests/glyph.rs index 9ac4f200f..f8bf723c3 100644 --- a/sparse_strips/vello_cpu/tests/glyph.rs +++ b/sparse_strips/vello_cpu/tests/glyph.rs @@ -81,7 +81,7 @@ fn glyphs_skewed() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) - .horizontal_skew(-20_f32.to_radians()) + .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.)) .hint(true) .fill_glyphs(glyphs.into_iter()); @@ -98,13 +98,93 @@ fn glyphs_skewed_unhinted() { ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); ctx.glyph_run(&font) .font_size(font_size) - .horizontal_skew(-20_f32.to_radians()) + .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.)) .hint(false) .fill_glyphs(glyphs.into_iter()); check_ref(&ctx, "glyphs_skewed_unhinted"); } +#[test] +fn glyphs_skewed_long() { + let mut ctx = get_ctx(250, 75, false); + let font_size: f32 = 20_f32; + let (font, glyphs) = layout_glyphs( + "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit.\nSed ornare arcu lectus.", + font_size, + ); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(-10_f64.to_radians().tan(), 0.)) + .hint(true) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_skewed_long"); +} + +#[test] +fn glyphs_skewed_long_unhinted() { + let mut ctx = get_ctx(250, 75, false); + let font_size: f32 = 20_f32; + let (font, glyphs) = layout_glyphs( + "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit.\nSed ornare arcu lectus.", + font_size, + ); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(-10_f64.to_radians().tan(), 0.)) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_skewed_long_unhinted"); +} + +#[test] +fn glyphs_skewed_unskewed() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform( + Affine::translate((0., f64::from(font_size))) + * Affine::skew(-20_f64.to_radians().tan(), 0.), + ); + ctx.set_paint(REBECCA_PURPLE.into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(20_f64.to_radians().tan(), 0.)) + .hint(true) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_skewed_unskewed"); +} + +#[test] +fn glyphs_skewed_unskewed_unhinted() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform( + Affine::translate((0., f64::from(font_size))) + * Affine::skew(-20_f64.to_radians().tan(), 0.), + ); + ctx.set_paint(REBECCA_PURPLE.into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(20_f64.to_radians().tan(), 0.)) + .hint(false) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "glyphs_skewed_unskewed_unhinted"); +} + #[test] fn glyphs_scaled() { let mut ctx = get_ctx(150, 125, false);