sparse_strips: Apply stroke adjustment (#1576)

If we have a glyph run with font size 8 and a scaling factor of 8, we
instead set the font size to 64 and the scale to 1. However, the stroke
width should _still_ be affected by the scaling factor, even if we
absorb. Unfortunately, this is not straightforward to do, because at the
time where we do absorption, we don't know yet whether we are going to
fill or stroke. So this is the only hack I could come up with.

There is one uncertainty: For glyph caching, we only do absorption if
there is no skewing and uniform scaling, so this is always safe to do.
However, for hinting we also can absorb if there is vertical skewing;
I'm not sure if uniformly increasing the stroke width is still the right
thing to do here? But anyway, this is hopefully an edge case we can
ignore for now.

Co-authored-by: Laurenz Stampfl <laurenz@canva.com>
diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs
index 67fe961..be0dc57 100644
--- a/glifo/src/glyph.rs
+++ b/glifo/src/glyph.rs
@@ -498,6 +498,18 @@
         }
     }
 
+    /// Return the scaling factor that should be applied to the stroke width when stroking this
+    /// glyph run.
+    pub fn stroke_adjustment(&self) -> f64 {
+        let run_size = self.prepared_run.run_size;
+
+        if run_size == 0.0 {
+            1.0
+        } else {
+            f64::from(self.prepared_run.draw_props.font_size / run_size)
+        }
+    }
+
     /// Render a decoration (like an underline) that skips over glyph descenders.
     ///
     /// This implements `text-decoration-skip-ink`-like behavior, where the decoration line is interrupted where it
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs
index c46620c..4e5ff26 100644
--- a/sparse_strips/vello_cpu/src/render.rs
+++ b/sparse_strips/vello_cpu/src/render.rs
@@ -469,11 +469,17 @@
         self.state.stroke = stroke;
     }
 
-    /// Get the current stroke
+    /// Get the current stroke.
     pub fn stroke(&self) -> &Stroke {
         &self.state.stroke
     }
 
+    /// Get a mutable reference to the current stroke.
+    #[cfg(feature = "text")]
+    pub(crate) fn stroke_mut(&mut self) -> &mut Stroke {
+        &mut self.state.stroke
+    }
+
     /// Set the current paint.
     ///
     /// If the paint is an image with `ImageSource::OpaqueId`, it will be
diff --git a/sparse_strips/vello_cpu/src/text.rs b/sparse_strips/vello_cpu/src/text.rs
index d713130..e5aebd1 100644
--- a/sparse_strips/vello_cpu/src/text.rs
+++ b/sparse_strips/vello_cpu/src/text.rs
@@ -262,7 +262,13 @@
     where
         Glyphs: Iterator<Item = Glyph> + Clone,
     {
-        self.render_glyphs(run, glyphs, |glyph_run, ctx| glyph_run.stroke_glyphs(ctx));
+        self.render_glyphs(run, glyphs, |glyph_run, ctx| {
+            let stroke_adjustment = glyph_run.stroke_adjustment();
+            let original_width = ctx.stroke().width;
+            ctx.stroke_mut().width *= stroke_adjustment;
+            glyph_run.stroke_glyphs(ctx);
+            ctx.stroke_mut().width = original_width;
+        });
     }
 }
 
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index 6803bbb..74296db 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -738,6 +738,17 @@
         self.render_state.stroke = stroke;
     }
 
+    /// Get the current stroke.
+    pub fn stroke(&self) -> &Stroke {
+        &self.render_state.stroke
+    }
+
+    /// Get a mutable reference to the current stroke.
+    #[cfg(feature = "text")]
+    pub(crate) fn stroke_mut(&mut self) -> &mut Stroke {
+        &mut self.render_state.stroke
+    }
+
     /// Set the paint for subsequent rendering operations.
     // TODO: This API is not final. Supporting images from a pixmap is explicitly out of scope.
     //       Instead images should be passed via a backend-agnostic opaque id, and be hydrated at
diff --git a/sparse_strips/vello_hybrid/src/text.rs b/sparse_strips/vello_hybrid/src/text.rs
index 15ad4be..282c926 100644
--- a/sparse_strips/vello_hybrid/src/text.rs
+++ b/sparse_strips/vello_hybrid/src/text.rs
@@ -251,7 +251,11 @@
         Glyphs: Iterator<Item = Glyph> + Clone,
     {
         self.render_glyphs(run, glyphs, |glyph_run, scene| {
+            let stroke_adjustment = glyph_run.stroke_adjustment();
+            let original_width = scene.stroke().width;
+            scene.stroke_mut().width *= stroke_adjustment;
             glyph_run.stroke_glyphs(scene);
+            scene.stroke_mut().width = original_width;
         });
     }
 }
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up.png
new file mode 100644
index 0000000..89f0b05
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27d06e778645c3a5c41e4ebdc1e2e22e173009bd59906710762ce43de9874908
+size 3761
diff --git a/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up_cached.png b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up_cached.png
new file mode 100644
index 0000000..89f0b05
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/glyphs_stroked_scaled_up_cached.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27d06e778645c3a5c41e4ebdc1e2e22e173009bd59906710762ce43de9874908
+size 3761
diff --git a/sparse_strips/vello_sparse_tests/tests/glyph.rs b/sparse_strips/vello_sparse_tests/tests/glyph.rs
index 4451716..e79bcaf 100644
--- a/sparse_strips/vello_sparse_tests/tests/glyph.rs
+++ b/sparse_strips/vello_sparse_tests/tests/glyph.rs
@@ -155,6 +155,24 @@
 }
 
 #[vello_test(width = 300, height = 70, glyph)]
+fn glyphs_stroked_scaled_up(ctx: &mut impl Renderer, enable_caching: bool) {
+    let font_size: f32 = 5_f32;
+    let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
+
+    ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(10.0));
+    ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
+    ctx.set_stroke(Stroke {
+        width: 0.3,
+        ..Stroke::default()
+    });
+    ctx.glyph_run(&font)
+        .font_size(font_size)
+        .atlas_cache(enable_caching)
+        .hint(false)
+        .stroke_glyphs(glyphs.into_iter());
+}
+
+#[vello_test(width = 300, height = 70, glyph)]
 fn glyphs_large_stroke_width(ctx: &mut impl Renderer, enable_caching: bool) {
     let font_size: f32 = 50_f32;
     let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);