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>>), 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 /// 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. /// applied to any points to ensure they are in the matching device space.
struct PathBuilderRef<'a, B: Backend> { struct PathBuilderRef<'a, B: Backend> {
@ -843,7 +825,9 @@ impl<'a, B: Backend> CanvasData<'a, B> {
self.path_builder().close(); 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 there's no record of any path yet, create a new builder in user-space.
if self.path_state.is_none() { if self.path_state.is_none() {
self.path_state = Some(PathState::UserSpacePathBuilder( self.path_state = Some(PathState::UserSpacePathBuilder(
@ -881,15 +865,8 @@ impl<'a, B: Backend> CanvasData<'a, B> {
// finished path by inverting the initial transformation. // finished path by inverting the initial transformation.
let new_state = match *self.path_state.as_mut().unwrap() { let new_state = match *self.path_state.as_mut().unwrap() {
PathState::DeviceSpacePathBuilder(ref mut builder) => { PathState::DeviceSpacePathBuilder(ref mut builder) => {
let path = builder.finish(); let inverse = self.drawtarget.get_transform().inverse()?;
let inverse = match self.drawtarget.get_transform().inverse() { let mut builder = builder.finish().transformed_copy_to_builder(&inverse);
Some(m) => m,
None => {
warn!("Couldn't invert canvas transformation.");
return;
},
};
let mut builder = path.transformed_copy_to_builder(&inverse);
Some(builder.finish()) Some(builder.finish())
}, },
PathState::UserSpacePathBuilder(..) | PathState::UserSpacePath(..) => None, PathState::UserSpacePathBuilder(..) | PathState::UserSpacePath(..) => None,
@ -898,14 +875,10 @@ impl<'a, B: Backend> CanvasData<'a, B> {
self.path_state = Some(PathState::UserSpacePath(path, None)); self.path_state = Some(PathState::UserSpacePath(path, None));
} }
assert!(self.path_state.as_ref().unwrap().is_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."),
fn path(&self) -> &B::Path { }
self.path_state
.as_ref()
.expect("Should have called ensure_path()")
.path()
} }
pub(crate) fn fill(&mut self) { 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. 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.drawtarget.fill(
&self.path().clone(), &path,
self.state.fill_style.clone(), self.state.fill_style.clone(),
&self.state.draw_options.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. 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.drawtarget.stroke(
&self.path().clone(), &path,
self.state.stroke_style.clone(), self.state.stroke_style.clone(),
&self.state.stroke_opts, &self.state.stroke_opts,
&self.state.draw_options, &self.state.draw_options,
@ -965,8 +944,10 @@ impl<'a, B: Backend> CanvasData<'a, B> {
} }
pub(crate) fn clip(&mut self) { pub(crate) fn clip(&mut self) {
self.ensure_path(); let Some(path) = self.ensure_path().cloned() else {
let path = self.path().clone(); return; // Path is uninvertible.
};
self.drawtarget.push_clip(&path); self.drawtarget.push_clip(&path);
} }

View file

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