Propagate document backgrounds to the canvas
This commit is contained in:
1 parent
14b427a825
commit
b69ce4156e
2 files changed
+299
-4
No files matched your search
@@ -2683,9 +2683,11 @@ mod tests {
|
|||||||
let (_, raster) = decode_capture(&off_viewport);
|
let (_, raster) = decode_capture(&off_viewport);
|
||||||
assert_eq!(raster.dimensions(), (20, 20));
|
assert_eq!(raster.dimensions(), (20, 20));
|
||||||
assert_eq!(raster.get_pixel(5, 10).0, [0, 0, 255, 255]);
|
assert_eq!(raster.get_pixel(5, 10).0, [0, 0, 255, 255]);
|
||||||
// TODO(canvas-background-transfer): Chromium propagates the body's red
|
assert_eq!(
|
||||||
// canvas background into the half beyond the 100px document box. This
|
raster.get_pixel(15, 10).0,
|
||||||
// regression covers acceptance and the represented clip content.
|
[255, 0, 0, 255],
|
||||||
|
"Chromium 145 propagates the body canvas background beyond its box"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
|
|||||||
@@ -2291,6 +2291,7 @@ fn paint_prepared_with_surface_color(
|
|||||||
surface_color[2],
|
surface_color[2],
|
||||||
surface_color[3],
|
surface_color[3],
|
||||||
));
|
));
|
||||||
|
let canvas_background = canvas_background_source(tree, &prepared.layout);
|
||||||
paint_laid_dom_scrolled(
|
paint_laid_dom_scrolled(
|
||||||
tree,
|
tree,
|
||||||
prepared.viewport,
|
prepared.viewport,
|
||||||
@@ -2314,6 +2315,7 @@ fn paint_prepared_with_surface_color(
|
|||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
1.0,
|
1.0,
|
||||||
false,
|
false,
|
||||||
|
canvas_background,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2357,6 +2359,7 @@ pub fn paint_prepared_with_scroll_and_surface_color(
|
|||||||
surface_color[2],
|
surface_color[2],
|
||||||
surface_color[3],
|
surface_color[3],
|
||||||
));
|
));
|
||||||
|
let canvas_background = canvas_background_source(tree, &prepared.layout);
|
||||||
paint_laid_dom_scrolled(
|
paint_laid_dom_scrolled(
|
||||||
tree,
|
tree,
|
||||||
prepared.viewport,
|
prepared.viewport,
|
||||||
@@ -2380,6 +2383,7 @@ pub fn paint_prepared_with_scroll_and_surface_color(
|
|||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
1.0,
|
1.0,
|
||||||
false,
|
false,
|
||||||
|
canvas_background,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2480,6 +2484,7 @@ fn paint_prepared_region_with_scroll_policy(
|
|||||||
// their live viewport's document-space position.
|
// their live viewport's document-space position.
|
||||||
let root = scroll.root_offset();
|
let root = scroll.root_offset();
|
||||||
let surface_offset = (root.0 - region.x, root.1 - region.y);
|
let surface_offset = (root.0 - region.x, root.1 - region.y);
|
||||||
|
let canvas_background = canvas_background_source(tree, &prepared.layout);
|
||||||
let pixmap = paint_laid_dom_scrolled(
|
let pixmap = paint_laid_dom_scrolled(
|
||||||
tree,
|
tree,
|
||||||
prepared.viewport,
|
prepared.viewport,
|
||||||
@@ -2503,6 +2508,7 @@ fn paint_prepared_region_with_scroll_policy(
|
|||||||
surface_offset,
|
surface_offset,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)
|
)
|
||||||
.ok_or(CaptureError::PaintFailed)?;
|
.ok_or(CaptureError::PaintFailed)?;
|
||||||
|
|
||||||
@@ -2733,6 +2739,164 @@ fn transform_subtree_source_bounds(
|
|||||||
/// Paint an already prepared layout without changing its document-space
|
/// Paint an already prepared layout without changing its document-space
|
||||||
/// geometry. Root scrolling and sticky positioning are per-shot visual state,
|
/// geometry. Root scrolling and sticky positioning are per-shot visual state,
|
||||||
/// so alternating captures can safely reuse the same layout.
|
/// so alternating captures can safely reuse the same layout.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct CanvasBackground {
|
||||||
|
root: obscura_dom::tree::NodeId,
|
||||||
|
source: obscura_dom::tree::NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_has_canvas_background(style: &crate::LayoutStyle) -> bool {
|
||||||
|
style.background_color.is_some_and(|color| color[3] != 0)
|
||||||
|
|| style.background_image.is_some()
|
||||||
|
|| style.background_gradient.is_some()
|
||||||
|
|| style.background_radial_gradient.is_some()
|
||||||
|
|| style.background_conic_gradient.is_some()
|
||||||
|
|| !style.background_gradient_layers.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canvas_background_source(tree: &DomTree, laid: &crate::DomLayout) -> Option<CanvasBackground> {
|
||||||
|
let root = tree.query_selector("html").ok().flatten()?;
|
||||||
|
let root_style = laid.styles.get(&root)?;
|
||||||
|
let root_is_contained = root_style.containing_block_triggers & crate::CB_TRIGGER_CONTAIN != 0;
|
||||||
|
if root_is_contained || style_has_canvas_background(root_style) {
|
||||||
|
return Some(CanvasBackground { root, source: root });
|
||||||
|
}
|
||||||
|
let body = tree.query_selector("body").ok().flatten();
|
||||||
|
let source = body
|
||||||
|
.filter(|body| {
|
||||||
|
laid.styles.get(body).is_some_and(|style| {
|
||||||
|
style.containing_block_triggers & crate::CB_TRIGGER_CONTAIN == 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(root);
|
||||||
|
Some(CanvasBackground { root, source })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_canvas_background(
|
||||||
|
pixmap: &mut Pixmap,
|
||||||
|
style: &crate::LayoutStyle,
|
||||||
|
origin_rect: &crate::Rect,
|
||||||
|
surface_rect: &crate::Rect,
|
||||||
|
root_font_size: f32,
|
||||||
|
viewport: (f32, f32),
|
||||||
|
base_url: Option<&str>,
|
||||||
|
image_cache: &mut RenderResourceCache,
|
||||||
|
raster_scale: f32,
|
||||||
|
) {
|
||||||
|
let Some(surface) = Rect::from_xywh(
|
||||||
|
surface_rect.x,
|
||||||
|
surface_rect.y,
|
||||||
|
surface_rect.width,
|
||||||
|
surface_rect.height,
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut builder = PathBuilder::new();
|
||||||
|
builder.push_rect(surface);
|
||||||
|
let Some(path) = builder.finish() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(color) = style.background_color {
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color(Color::from_rgba8(
|
||||||
|
color[0], color[1], color[2], color[3],
|
||||||
|
));
|
||||||
|
pixmap.fill_path(
|
||||||
|
&path,
|
||||||
|
&paint,
|
||||||
|
FillRule::Winding,
|
||||||
|
raster_transform(raster_scale),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !style.background_gradient_layers.is_empty() {
|
||||||
|
paint_background_gradient_layers(
|
||||||
|
pixmap,
|
||||||
|
&path,
|
||||||
|
origin_rect,
|
||||||
|
surface_rect,
|
||||||
|
crate::ResolvedBorderRadii::default(),
|
||||||
|
style,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
None,
|
||||||
|
raster_scale,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if let Some((center, stops)) = &style.background_radial_gradient {
|
||||||
|
paint_radial_gradient(
|
||||||
|
pixmap,
|
||||||
|
&path,
|
||||||
|
origin_rect,
|
||||||
|
*center,
|
||||||
|
stops,
|
||||||
|
style.background_radial_gradient_geometry,
|
||||||
|
style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
None,
|
||||||
|
raster_scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some((angle, center, stops)) = &style.background_conic_gradient {
|
||||||
|
paint_conic_gradient_sampled(
|
||||||
|
pixmap,
|
||||||
|
surface_rect,
|
||||||
|
origin_rect,
|
||||||
|
crate::ResolvedBorderRadii::default(),
|
||||||
|
*angle,
|
||||||
|
*center,
|
||||||
|
stops,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some((angle, stops)) = &style.background_gradient {
|
||||||
|
paint_linear_gradient(
|
||||||
|
pixmap,
|
||||||
|
&path,
|
||||||
|
origin_rect,
|
||||||
|
*angle,
|
||||||
|
stops,
|
||||||
|
None,
|
||||||
|
raster_scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = &style.background_image {
|
||||||
|
if let Some(image_rect) = background_image_rect(
|
||||||
|
url,
|
||||||
|
base_url,
|
||||||
|
origin_rect,
|
||||||
|
style.background_size,
|
||||||
|
style.background_size_expression.as_deref(),
|
||||||
|
style.background_size_fit,
|
||||||
|
style.background_position,
|
||||||
|
style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
image_cache,
|
||||||
|
) {
|
||||||
|
paint_image(
|
||||||
|
url,
|
||||||
|
base_url,
|
||||||
|
&image_rect,
|
||||||
|
surface_rect,
|
||||||
|
crate::ObjectFit::Fill,
|
||||||
|
crate::ObjectPosition::default(),
|
||||||
|
pixmap,
|
||||||
|
image_cache,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
crate::ResolvedBorderRadii::default(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_laid_dom_scrolled(
|
fn paint_laid_dom_scrolled(
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
viewport: (f32, f32),
|
viewport: (f32, f32),
|
||||||
@@ -2756,6 +2920,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
surface_offset: (f32, f32),
|
surface_offset: (f32, f32),
|
||||||
raster_scale: f32,
|
raster_scale: f32,
|
||||||
print_economy: bool,
|
print_economy: bool,
|
||||||
|
canvas_background: Option<CanvasBackground>,
|
||||||
) -> Option<Pixmap> {
|
) -> Option<Pixmap> {
|
||||||
let scroll_state = match resolved_scroll {
|
let scroll_state = match resolved_scroll {
|
||||||
Some(resolved) => ScrollPaintState::from_resolved(
|
Some(resolved) => ScrollPaintState::from_resolved(
|
||||||
@@ -2786,6 +2951,40 @@ fn paint_laid_dom_scrolled(
|
|||||||
.and_then(|root| laid.styles.get(&root))
|
.and_then(|root| laid.styles.get(&root))
|
||||||
.and_then(|style| style.font_size)
|
.and_then(|style| style.font_size)
|
||||||
.unwrap_or(16.0);
|
.unwrap_or(16.0);
|
||||||
|
if paint_root.is_none() {
|
||||||
|
if let Some(canvas) = canvas_background {
|
||||||
|
let root_visible = laid
|
||||||
|
.styles
|
||||||
|
.get(&canvas.root)
|
||||||
|
.is_some_and(|style| !style.effectively_invisible);
|
||||||
|
if root_visible {
|
||||||
|
if let (Some(style), Some(source_rect)) = (
|
||||||
|
laid.styles.get(&canvas.source),
|
||||||
|
laid.rects.get(&canvas.source).copied(),
|
||||||
|
) {
|
||||||
|
let (x, y) = scroll_state.translation_for(laid, canvas.source);
|
||||||
|
let origin_rect = crate::Rect {
|
||||||
|
x: source_rect.x + x,
|
||||||
|
y: source_rect.y + y,
|
||||||
|
width: source_rect.width,
|
||||||
|
height: source_rect.height,
|
||||||
|
};
|
||||||
|
let surface_rect = paint_surface_rect(&pixmap, raster_scale);
|
||||||
|
paint_canvas_background(
|
||||||
|
&mut pixmap,
|
||||||
|
style,
|
||||||
|
&origin_rect,
|
||||||
|
&surface_rect,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
base_url,
|
||||||
|
image_cache,
|
||||||
|
raster_scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Nodes that live inside an inline `<svg>` we rasterized as one document;
|
// Nodes that live inside an inline `<svg>` we rasterized as one document;
|
||||||
// their painting is owned by that raster, so they are skipped in both the
|
// their painting is owned by that raster, so they are skipped in both the
|
||||||
// box/text loop below and the inline-formatting loop after it (an svg
|
// box/text loop below and the inline-formatting loop after it (an svg
|
||||||
@@ -2983,6 +3182,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
surface_offset,
|
surface_offset,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)?;
|
)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3016,6 +3216,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
surface_offset,
|
surface_offset,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)?;
|
)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3116,6 +3317,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
surface_offset,
|
surface_offset,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)?;
|
)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3191,6 +3393,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
),
|
),
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)?;
|
)?;
|
||||||
let transform =
|
let transform =
|
||||||
display_transform.then(crate::Affine2::translate(-layer_delta.0, -layer_delta.1));
|
display_transform.then(crate::Affine2::translate(-layer_delta.0, -layer_delta.1));
|
||||||
@@ -3261,6 +3464,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
surface_offset,
|
surface_offset,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
print_economy,
|
print_economy,
|
||||||
|
canvas_background,
|
||||||
)?;
|
)?;
|
||||||
let group_paint = tiny_skia::PixmapPaint {
|
let group_paint = tiny_skia::PixmapPaint {
|
||||||
opacity: own_opacity,
|
opacity: own_opacity,
|
||||||
@@ -3280,6 +3484,8 @@ fn paint_laid_dom_scrolled(
|
|||||||
if style.effectively_invisible {
|
if style.effectively_invisible {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let background_transfers_to_canvas = canvas_background
|
||||||
|
.is_some_and(|canvas| nid == canvas.root || nid == canvas.source);
|
||||||
|
|
||||||
// A `transform: translate()` on this element or any ancestor offsets
|
// A `transform: translate()` on this element or any ancestor offsets
|
||||||
// this element's whole painted box (and, applied per node, its whole
|
// this element's whole painted box (and, applied per node, its whole
|
||||||
@@ -3438,6 +3644,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
// not paint as a box here; the text paint path fills the glyphs instead.
|
// not paint as a box here; the text paint path fills the glyphs instead.
|
||||||
if box_on_surface
|
if box_on_surface
|
||||||
&& !paints_inline_fragments
|
&& !paints_inline_fragments
|
||||||
|
&& !background_transfers_to_canvas
|
||||||
&& style.mask_image.is_none()
|
&& style.mask_image.is_none()
|
||||||
&& !style.background_clip_text
|
&& !style.background_clip_text
|
||||||
{
|
{
|
||||||
@@ -3516,7 +3723,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if box_on_surface && !paints_inline_fragments {
|
if box_on_surface && !paints_inline_fragments && !background_transfers_to_canvas {
|
||||||
if let Some(mask_url) = &style.mask_image {
|
if let Some(mask_url) = &style.mask_image {
|
||||||
let fill = style
|
let fill = style
|
||||||
.background_color
|
.background_color
|
||||||
@@ -10656,6 +10863,92 @@ mod tests {
|
|||||||
assert_eq!(outside.blue(), 255);
|
assert_eq!(outside.blue(), 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn body_background_transfers_to_canvas_once_over_base_surface() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html style="margin:0;background:transparent">
|
||||||
|
<body style="margin:0;width:20px;height:20px;background:rgba(255,0,0,.5)"></body>
|
||||||
|
</html>"#,
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom_scrolled_at_animation_time_with_surface_color(
|
||||||
|
&tree,
|
||||||
|
(100.0, 80.0),
|
||||||
|
None,
|
||||||
|
(0.0, 0.0),
|
||||||
|
crate::AnimationSampleTime::default(),
|
||||||
|
[0, 0, 255, 255],
|
||||||
|
)
|
||||||
|
.expect("canvas paint");
|
||||||
|
let inside = pixmap.pixel(10, 10).expect("inside body box");
|
||||||
|
let outside = pixmap.pixel(90, 70).expect("outside body box");
|
||||||
|
assert_eq!(inside, outside, "transferred body background must not paint twice");
|
||||||
|
assert!((127..=128).contains(&inside.red()), "red blend: {inside:?}");
|
||||||
|
assert_eq!(inside.green(), 0);
|
||||||
|
assert!((127..=128).contains(&inside.blue()), "blue blend: {inside:?}");
|
||||||
|
assert_eq!(inside.alpha(), 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authored_html_background_owns_canvas_and_body_keeps_its_box_background() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html style="margin:0;background:rgb(10,20,30)">
|
||||||
|
<body style="margin:0;width:20px;height:20px;background:rgb(200,100,50)"></body>
|
||||||
|
</html>"#,
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom(&tree, (100.0, 80.0), None).expect("canvas paint");
|
||||||
|
let inside = pixmap.pixel(10, 10).expect("body pixel");
|
||||||
|
let outside = pixmap.pixel(90, 70).expect("canvas pixel");
|
||||||
|
assert_eq!(
|
||||||
|
(inside.red(), inside.green(), inside.blue()),
|
||||||
|
(200, 100, 50)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(outside.red(), outside.green(), outside.blue()),
|
||||||
|
(10, 20, 30)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transparent_html_and_body_leave_base_surface_unchanged() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html style="margin:0;background:transparent">
|
||||||
|
<body style="margin:0;background:transparent"></body></html>"#,
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom_scrolled_at_animation_time_with_surface_color(
|
||||||
|
&tree,
|
||||||
|
(40.0, 30.0),
|
||||||
|
None,
|
||||||
|
(0.0, 0.0),
|
||||||
|
crate::AnimationSampleTime::default(),
|
||||||
|
[4, 8, 12, 255],
|
||||||
|
)
|
||||||
|
.expect("transparent canvas paint");
|
||||||
|
for pixel in pixmap.pixels() {
|
||||||
|
assert_eq!(
|
||||||
|
(pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()),
|
||||||
|
(4, 8, 12, 255)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transferred_canvas_background_paints_below_negative_z_content() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html style="margin:0;background:transparent">
|
||||||
|
<body style="margin:0;background:red">
|
||||||
|
<div style="position:absolute;z-index:-1;left:0;top:0;width:20px;height:20px;background:blue"></div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let pixmap = paint_dom(&tree, (60.0, 40.0), None).expect("canvas paint");
|
||||||
|
let negative = pixmap.pixel(10, 10).expect("negative layer pixel");
|
||||||
|
let canvas = pixmap.pixel(40, 30).expect("canvas pixel");
|
||||||
|
assert_eq!(
|
||||||
|
(negative.red(), negative.green(), negative.blue()),
|
||||||
|
(0, 0, 255)
|
||||||
|
);
|
||||||
|
assert_eq!((canvas.red(), canvas.green(), canvas.blue()), (255, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn float_paints_above_normal_block_backgrounds_and_below_later_flow() {
|
fn float_paints_above_normal_block_backgrounds_and_below_later_flow() {
|
||||||
// Chromium 145: ordinary block border boxes remain full-width beneath
|
// Chromium 145: ordinary block border boxes remain full-width beneath
|
||||||
|
|||||||
Reference in new issue
Block a user