canvas: Don't do operations on paths with uninvertible transforms (#37551)

When the path is created with an uninvertible transform, don't do any
path-based operations. Now `ensure_path()` returns a `Path` object and
this change adds early returns which prevent trying to use it.

Testing: This change adds a WPT crash test.
Fixes #36995.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-06-19 12:48:20 +02:00 committed by GitHub
parent a27c9ee691
commit e5c328d660
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 42 deletions

View file

@ -128,24 +128,6 @@ enum PathState<B: Backend> {
UserSpacePath(B::Path, Option<Transform2D<f32>>),
}
impl<B: Backend> PathState<B> {
fn is_path(&self) -> bool {
match *self {
PathState::UserSpacePath(..) => true,
PathState::UserSpacePathBuilder(..) | PathState::DeviceSpacePathBuilder(..) => false,
}
}
fn path(&self) -> &B::Path {
match *self {
PathState::UserSpacePath(ref p, _) => p,
PathState::UserSpacePathBuilder(..) | PathState::DeviceSpacePathBuilder(..) => {
panic!("should have called ensure_path")
},
}
}
}
/// A wrapper around a stored PathBuilder and an optional transformation that should be
/// applied to any points to ensure they are in the matching device space.
struct PathBuilderRef<'a, B: Backend> {
@ -843,7 +825,9 @@ impl<'a, B: Backend> CanvasData<'a, B> {
self.path_builder().close();
}
fn ensure_path(&mut self) {
/// Turn the [`Self::path_state`] into a user-space path, returning `None` if the
/// path transformation matrix is uninvertible.
fn ensure_path(&mut self) -> Option<&B::Path> {
// If there's no record of any path yet, create a new builder in user-space.
if self.path_state.is_none() {
self.path_state = Some(PathState::UserSpacePathBuilder(
@ -881,15 +865,8 @@ impl<'a, B: Backend> CanvasData<'a, B> {
// finished path by inverting the initial transformation.
let new_state = match *self.path_state.as_mut().unwrap() {
PathState::DeviceSpacePathBuilder(ref mut builder) => {
let path = builder.finish();
let inverse = match self.drawtarget.get_transform().inverse() {
Some(m) => m,
None => {
warn!("Couldn't invert canvas transformation.");
return;
},
};
let mut builder = path.transformed_copy_to_builder(&inverse);
let inverse = self.drawtarget.get_transform().inverse()?;
let mut builder = builder.finish().transformed_copy_to_builder(&inverse);
Some(builder.finish())
},
PathState::UserSpacePathBuilder(..) | PathState::UserSpacePath(..) => None,
@ -898,14 +875,10 @@ impl<'a, B: Backend> CanvasData<'a, B> {
self.path_state = Some(PathState::UserSpacePath(path, None));
}
assert!(self.path_state.as_ref().unwrap().is_path())
}
fn path(&self) -> &B::Path {
self.path_state
.as_ref()
.expect("Should have called ensure_path()")
.path()
match self.path_state.as_ref() {
Some(PathState::UserSpacePath(path, _)) => Some(path),
_ => unreachable!("Should have been able to successful build path or returned early."),
}
}
pub(crate) fn fill(&mut self) {
@ -913,9 +886,12 @@ impl<'a, B: Backend> CanvasData<'a, B> {
return; // Paint nothing if gradient size is zero.
}
self.ensure_path();
let Some(path) = self.ensure_path().cloned() else {
return; // Path is uninvertible.
};
self.drawtarget.fill(
&self.path().clone(),
&path,
self.state.fill_style.clone(),
&self.state.draw_options.clone(),
);
@ -940,9 +916,12 @@ impl<'a, B: Backend> CanvasData<'a, B> {
return; // Paint nothing if gradient size is zero.
}
self.ensure_path();
let Some(path) = self.ensure_path().cloned() else {
return; // Path is uninvertible.
};
self.drawtarget.stroke(
&self.path().clone(),
&path,
self.state.stroke_style.clone(),
&self.state.stroke_opts,
&self.state.draw_options,
@ -965,8 +944,10 @@ impl<'a, B: Backend> CanvasData<'a, B> {
}
pub(crate) fn clip(&mut self) {
self.ensure_path();
let path = self.path().clone();
let Some(path) = self.ensure_path().cloned() else {
return; // Path is uninvertible.
};
self.drawtarget.push_clip(&path);
}

View file

@ -7814,6 +7814,15 @@
],
"element": {
"manual": {
"drawing-paths-to-the-canvas": {
"fill-path-with-uninvertible-transform-crash.html": [
"9d032ddba9637c8bee07c8cb9c4ec618a1c5d701",
[
null,
{}
]
]
},
"filters": {
"svg-filter-crash.html": [
"f64379c79241409ab55535596f4cbad894f45b42",

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<script>
let context = document.createElement("canvas").getContext("2d");
context.moveTo(0, 0);
context.setTransform(4, 2, 6, 3, 0, 0); // non-invertible (4 * 3 - 6 * 2 = 0)
context.fill("nonzero");
</script>