canvas: Properly bound all image pattern axis by inserting clip (#37668)

Before we only handled no-repeat for rect, this means we rendered
https://sagudev.github.io/briefcase/no-repeat.html incorrectly (like
firefox). Now if one of the axis is bounded (does not repeat) we clip it
and let other axis be unbounded (technically we clip it to end of
canvas).

This is also needed for vello backend.

Testing: Tests in WPT exists and another test is added.

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
sagudev 2025-07-11 08:49:09 +02:00 committed by GitHub
parent 464d71ecfc
commit 75c13f1422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 314 additions and 70 deletions

View file

@ -98,6 +98,7 @@ pub(crate) trait GenericDrawTarget<B: Backend> {
fn get_transform(&self) -> Transform2D<f32>;
fn pop_clip(&mut self);
fn push_clip(&mut self, path: &B::Path);
fn push_clip_rect(&mut self, rect: &Rect<i32>);
fn set_transform(&mut self, matrix: &Transform2D<f32>);
fn stroke(
&mut self,
@ -309,11 +310,13 @@ pub(crate) trait GenericPath<B: Backend<Path = Self>> {
}
}
}
fn bounding_box(&self) -> Rect<f64>;
}
pub(crate) trait PatternHelpers {
fn is_zero_size_gradient(&self) -> bool;
fn draw_rect(&self, rect: &Rect<f32>) -> Rect<f32>;
fn x_bound(&self) -> Option<u32>;
fn y_bound(&self) -> Option<u32>;
}
pub(crate) trait StrokeOptionsHelpers {
@ -327,4 +330,5 @@ pub(crate) trait StrokeOptionsHelpers {
pub(crate) trait DrawOptionsHelpers {
fn set_alpha(&mut self, val: f32);
fn is_clear(&self) -> bool;
}

View file

@ -492,11 +492,17 @@ impl<'a, B: Backend> CanvasData<'a, B> {
// > from left to right (if any), adding to the array, for each glyph, the shape of the glyph
// > as it is in the inline box, positioned on a coordinate space using CSS pixels with its
// > origin is at the anchor point.
self.drawtarget.fill_text(
shaped_runs,
start,
&self.state.fill_style,
&self.state.draw_options,
self.maybe_bound_shape_with_pattern(
self.state.fill_style.clone(),
&Rect::from_size(Size2D::new(total_advance, size)),
|self_| {
self_.drawtarget.fill_text(
shaped_runs,
start,
&self_.state.fill_style,
&self_.state.draw_options,
);
},
);
}
@ -676,19 +682,22 @@ impl<'a, B: Backend> CanvasData<'a, B> {
return; // Paint nothing if gradient size is zero.
}
let draw_rect = self.state.fill_style.draw_rect(rect);
if self.need_to_draw_shadow() {
self.draw_with_shadow(&draw_rect, |new_draw_target: &mut B::DrawTarget| {
new_draw_target.fill_rect(
&draw_rect,
&self.state.fill_style,
&self.state.draw_options,
);
self.draw_with_shadow(rect, |new_draw_target: &mut B::DrawTarget| {
new_draw_target.fill_rect(rect, &self.state.fill_style, &self.state.draw_options);
});
} else {
self.drawtarget
.fill_rect(&draw_rect, &self.state.fill_style, &self.state.draw_options);
self.maybe_bound_shape_with_pattern(
self.state.fill_style.clone(),
&rect.cast(),
|self_| {
self_.drawtarget.fill_rect(
rect,
&self_.state.fill_style,
&self_.state.draw_options,
);
},
);
}
}
@ -711,12 +720,18 @@ impl<'a, B: Backend> CanvasData<'a, B> {
);
});
} else {
self.drawtarget.stroke_rect(
rect,
&self.state.stroke_style,
&self.state.stroke_opts,
&self.state.draw_options,
);
self.maybe_bound_shape_with_pattern(
self.state.stroke_style.clone(),
&rect.cast(),
|self_| {
self_.drawtarget.stroke_rect(
rect,
&self_.state.stroke_style,
&self_.state.stroke_opts,
&self_.state.draw_options,
);
},
)
}
}
@ -781,11 +796,17 @@ impl<'a, B: Backend> CanvasData<'a, B> {
return; // Path is uninvertible.
};
self.drawtarget.fill(
&path,
&self.state.fill_style,
&self.state.draw_options.clone(),
);
self.maybe_bound_shape_with_pattern(
self.state.fill_style.clone(),
&path.bounding_box(),
|self_| {
self_.drawtarget.fill(
&path,
&self_.state.fill_style,
&self_.state.draw_options.clone(),
);
},
)
}
pub(crate) fn fill_path(&mut self, path_segments: &[PathSegment]) {
@ -809,12 +830,18 @@ impl<'a, B: Backend> CanvasData<'a, B> {
return; // Path is uninvertible.
};
self.drawtarget.stroke(
&path,
&self.state.stroke_style,
&self.state.stroke_opts,
&self.state.draw_options,
);
self.maybe_bound_shape_with_pattern(
self.state.stroke_style.clone(),
&path.bounding_box(),
|self_| {
self_.drawtarget.stroke(
&path,
&self_.state.stroke_style,
&self_.state.stroke_opts,
&self_.state.draw_options,
);
},
)
}
pub(crate) fn stroke_path(&mut self, path_segments: &[PathSegment]) {
@ -825,12 +852,18 @@ impl<'a, B: Backend> CanvasData<'a, B> {
let mut path = B::Path::new();
path.add_segments(path_segments);
self.drawtarget.stroke(
&path,
&self.state.stroke_style,
&self.state.stroke_opts,
&self.state.draw_options,
);
self.maybe_bound_shape_with_pattern(
self.state.stroke_style.clone(),
&path.bounding_box(),
|self_| {
self_.drawtarget.stroke(
&path,
&self_.state.stroke_style,
&self_.state.stroke_opts,
&self_.state.draw_options,
);
},
)
}
pub(crate) fn clip(&mut self) {
@ -1183,6 +1216,34 @@ impl<'a, B: Backend> CanvasData<'a, B> {
);
}
/// Push a clip to the draw target to respect the non-repeating bound (either x, y, or both)
/// of the given pattern.
fn maybe_bound_shape_with_pattern<F>(
&mut self,
pattern: B::Pattern<'_>,
path_bound_box: &Rect<f64>,
draw_shape: F,
) where
F: FnOnce(&mut Self),
{
let x_bound = pattern.x_bound();
let y_bound = pattern.y_bound();
// Clear operations are also unbounded.
if self.state.draw_options.is_clear() || (x_bound.is_none() && y_bound.is_none()) {
draw_shape(self);
return;
}
let rect = Rect::from_size(Size2D::new(
x_bound.unwrap_or(path_bound_box.size.width.ceil() as u32),
y_bound.unwrap_or(path_bound_box.size.height.ceil() as u32),
))
.cast();
let rect = self.get_transform().outer_transformed_rect(&rect);
self.drawtarget.push_clip_rect(&rect.cast());
draw_shape(self);
self.drawtarget.pop_clip();
}
/// It reads image data from the canvas
/// canvas_size: The size of the canvas we're reading from
/// read_rect: The area of the canvas we want to read from

View file

@ -8,7 +8,7 @@ use std::collections::HashMap;
use canvas_traits::canvas::*;
use compositing_traits::SerializableImageData;
use cssparser::color::clamp_unit_f32;
use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D};
use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D};
use font_kit::font::Font;
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods};
use ipc_channel::ipc::IpcSharedMemory;
@ -197,15 +197,13 @@ impl SurfacePattern {
transform,
}
}
pub fn size(&self) -> Size2D<f32> {
self.image.size().cast()
}
pub fn repetition(&self) -> &Repetition {
&self.repeat
}
}
#[derive(Clone)]
#[derive(Clone, Copy, Debug)]
pub enum Repetition {
Repeat,
RepeatX,
@ -284,33 +282,23 @@ impl PatternHelpers for Pattern {
}
}
fn draw_rect(&self, rect: &Rect<f32>) -> Rect<f32> {
fn x_bound(&self) -> Option<u32> {
match self {
Pattern::Surface(pattern) => {
let pattern_rect = Rect::new(Point2D::origin(), pattern.size());
let mut draw_rect = rect.intersection(&pattern_rect).unwrap_or(Rect::zero());
match pattern.repetition() {
Repetition::NoRepeat => {
draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width);
draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height);
},
Repetition::RepeatX => {
draw_rect.size.width = rect.size.width;
draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height);
},
Repetition::RepeatY => {
draw_rect.size.height = rect.size.height;
draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width);
},
Repetition::Repeat => {
draw_rect = *rect;
},
}
draw_rect
Pattern::Surface(pattern) => match pattern.repetition() {
Repetition::RepeatX | Repetition::Repeat => None, // x is not bounded
Repetition::RepeatY | Repetition::NoRepeat => Some(pattern.image.size().width),
},
Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => *rect,
Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => None,
}
}
fn y_bound(&self) -> Option<u32> {
match self {
Pattern::Surface(pattern) => match pattern.repetition() {
Repetition::RepeatY | Repetition::Repeat => None, // y is not bounded
Repetition::RepeatX | Repetition::NoRepeat => Some(pattern.image.size().height),
},
Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => None,
}
}
}
@ -340,6 +328,10 @@ impl DrawOptionsHelpers for raqote::DrawOptions {
fn set_alpha(&mut self, val: f32) {
self.alpha = val;
}
fn is_clear(&self) -> bool {
matches!(self.blend_mode, raqote::BlendMode::Clear)
}
}
fn create_gradient_stops(gradient_stops: Vec<CanvasGradientStop>) -> Vec<raqote::GradientStop> {
@ -576,6 +568,9 @@ impl GenericDrawTarget<RaqoteBackend> for raqote::DrawTarget {
fn push_clip(&mut self, path: &<RaqoteBackend as Backend>::Path) {
self.push_clip(&path.into());
}
fn push_clip_rect(&mut self, rect: &Rect<i32>) {
self.push_clip_rect(rect.to_box2d());
}
fn set_transform(&mut self, matrix: &Transform2D<f32>) {
self.set_transform(matrix);
}
@ -730,6 +725,28 @@ impl GenericPath<RaqoteBackend> for Path {
let path: raqote::Path = (&*self).into();
self.0.set(path.transform(transform).into());
}
fn bounding_box(&self) -> Rect<f64> {
let path: raqote::Path = self.into();
let mut points = vec![];
for op in path.ops {
match op {
PathOp::MoveTo(p) => points.push(p),
PathOp::LineTo(p) => points.push(p),
PathOp::QuadTo(p1, p2) => {
points.push(p1);
points.push(p2);
},
PathOp::CubicTo(p1, p2, p3) => {
points.push(p1);
points.push(p2);
points.push(p3);
},
PathOp::Close => {},
}
}
Box2D::from_points(points).to_rect().cast()
}
}
fn get_first_point(op: &PathOp) -> Option<euclid::Point2D<f32, euclid::UnknownUnit>> {