Model embedded frame base backgrounds

This commit is contained in:
ldm0
2026-08-27 00:54:04 +08:00
parent d0b3d4ef0a
commit 9cddb2d698
6 changed files with 97 additions and 10 deletions
+32 -1
View File
@@ -1,4 +1,6 @@
use crate::{LayoutError, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform2D, LayoutViewport};
use crate::{
LayoutError, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform2D, LayoutViewport, PaintColor,
};
/// Exclusive CSS-pixel limit for either side of an automatic full-document
/// capture, matching Chromium's `Page.captureScreenshot` boundary.
@@ -31,6 +33,12 @@ pub struct PaintCaptureRequest {
/// distinction outside [`PaintCaptureRegion`], because both use the same
/// document-coordinate raster rectangle.
pub include_viewport_controls: bool,
/// Color behind the document canvas before CSS canvas propagation.
///
/// Main-frame captures use an opaque white browser surface. Embedded
/// browsing contexts use transparent black so their owner element remains
/// visible unless the child document supplies its own canvas background.
pub base_background_color: PaintColor,
/// Maximum encoded image width in device pixels.
pub max_width: Option<u32>,
/// Maximum encoded image height in device pixels.
@@ -44,6 +52,7 @@ impl PaintCaptureRequest {
region: PaintCaptureRegion::Viewport,
include_backgrounds: true,
include_viewport_controls: true,
base_background_color: PaintColor::WHITE,
max_width: None,
max_height: None,
}
@@ -55,6 +64,7 @@ impl PaintCaptureRequest {
region: PaintCaptureRegion::Viewport,
include_backgrounds: true,
include_viewport_controls: true,
base_background_color: PaintColor::WHITE,
max_width,
max_height,
}
@@ -66,6 +76,7 @@ impl PaintCaptureRequest {
region: PaintCaptureRegion::FullDocument,
include_backgrounds: true,
include_viewport_controls: false,
base_background_color: PaintColor::WHITE,
max_width: None,
max_height: None,
}
@@ -77,6 +88,7 @@ impl PaintCaptureRequest {
region: PaintCaptureRegion::PageClip { rect, scale },
include_backgrounds: true,
include_viewport_controls: false,
base_background_color: PaintColor::WHITE,
max_width: None,
max_height: None,
}
@@ -125,6 +137,7 @@ pub(crate) struct ResolvedPaintCapture {
/// document and page-clip captures paint page content only, matching
/// Chromium's compositor capture behavior.
pub(crate) paint_root_scrollbars: bool,
pub(crate) base_background_color: PaintColor,
}
impl PaintCaptureRequest {
@@ -201,6 +214,7 @@ impl PaintCaptureRequest {
),
include_backgrounds: self.include_backgrounds,
paint_root_scrollbars,
base_background_color: self.base_background_color,
})
}
}
@@ -289,6 +303,22 @@ mod tests {
LayoutPoint::ZERO
);
assert!(!capture.paint_root_scrollbars);
assert_eq!(capture.base_background_color, PaintColor::WHITE);
}
#[test]
fn embedded_capture_can_select_a_transparent_base_background() {
let mut request = PaintCaptureRequest::viewport();
request.base_background_color = PaintColor::TRANSPARENT;
let capture = request
.resolve(
LayoutViewport::new(100, 80, 1.0),
LayoutPoint::ZERO,
LayoutSize::new(100.0, 80.0),
)
.expect("embedded viewport capture");
assert_eq!(capture.base_background_color, PaintColor::TRANSPARENT);
}
#[test]
@@ -300,6 +330,7 @@ mod tests {
},
include_backgrounds: true,
include_viewport_controls: false,
base_background_color: PaintColor::WHITE,
max_width: Some(500),
max_height: Some(200),
}
+5 -4
View File
@@ -79,9 +79,9 @@ where
{
let root = projection.world.root;
let (canvas_color, propagated_background) = if capture.include_backgrounds {
canvas_background(projection.world, root)
canvas_background(projection.world, root, capture.base_background_color)
} else {
(PaintColor::WHITE, None)
(capture.base_background_color, None)
};
let mut snapshot = PaintSnapshot::new(projection.viewport, canvas_color);
snapshot.surface = capture.surface;
@@ -421,6 +421,7 @@ fn pop_clips(count: usize, snapshot: &mut PaintSnapshot) {
fn canvas_background<N>(
world: &LayoutWorld<N>,
root: LayoutBoxId,
base_background_color: PaintColor,
) -> (PaintColor, Option<LayoutBoxId>)
where
N: Copy + Debug + Eq + Hash,
@@ -430,7 +431,7 @@ where
.element_semantics()
.is_some_and(|element| element.is_html_element("html"));
if !is_html_root {
return (PaintColor::WHITE, None);
return (base_background_color, None);
}
if style_has_canvas_background(&root_box.style) {
return (root_box.style.background_color(), Some(root));
@@ -450,7 +451,7 @@ where
}
stack.extend(layout_box.children.iter().rev().copied());
}
(PaintColor::WHITE, None)
(base_background_color, None)
}
fn style_has_canvas_background(style: &crate::ResolvedLayoutStyle) -> bool {
+7 -5
View File
@@ -1020,11 +1020,13 @@ impl PaintSnapshot {
shape: PaintShape::Rect(clip),
transform: local_to_surface,
});
self.push_fragment(PaintFragment::Fill {
shape: PaintShape::Rect(clip),
brush: PaintBrush::Solid(child.canvas_color),
transform: local_to_surface,
});
if child.canvas_color.alpha > 0.0 {
self.push_fragment(PaintFragment::Fill {
shape: PaintShape::Rect(clip),
brush: PaintBrush::Solid(child.canvas_color),
transform: local_to_surface,
});
}
for fragment in child.fragments {
if let Some(fragment) = rebase_embedded_fragment(
fragment,
@@ -113,6 +113,7 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
let request = if self.capture_paint {
let mut capture = moli_layout::PaintCaptureRequest::viewport();
capture.include_backgrounds = self.include_backgrounds;
capture.base_background_color = moli_layout::PaintColor::TRANSPARENT;
LayoutPassRequest::with_capture(viewport, self.reason, capture)
} else {
LayoutPassRequest::new(viewport, self.reason)
@@ -213,6 +213,7 @@ impl PageVm {
region: moli_layout::PaintCaptureRegion::Viewport,
include_backgrounds: true,
include_viewport_controls: true,
base_background_color: moli_layout::PaintColor::WHITE,
max_width: request.max_width,
max_height: request.max_height,
};
@@ -356,6 +357,7 @@ impl RendererCaptureScreenshotRequest {
RendererScreenshotPurpose::Screenshot => true,
},
include_viewport_controls,
base_background_color: moli_layout::PaintColor::WHITE,
max_width: self.max_width,
max_height: self.max_height,
})
@@ -799,6 +799,56 @@ nestedDocument.body.appendChild(nestedViewportSized);
.expect("iframe snapshot composition fixture should run");
}
#[tokio::test(flavor = "current_thread")]
async fn screenshot_composites_transparent_iframe_canvas_over_its_owner_background() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/transparent-iframe-canvas.html")?,
);
page_vm.vm_mut().eval(
r#"
document.documentElement.style.cssText = 'margin:0;padding:0;background:white';
document.body.style.cssText = 'margin:0;padding:0';
document.body.innerHTML = `
<iframe id=transparent style="position:absolute;left:0;top:0;display:block;width:100px;aspect-ratio:1/1;border:0;background:green"></iframe>
<iframe id=colored style="position:absolute;left:110px;top:0;display:block;width:100px;aspect-ratio:1/1;border:0;background:green"></iframe>`;
const colored = document.getElementById('colored').contentDocument;
colored.documentElement.style.cssText = 'margin:0;padding:0;background:blue';
'installed'
"#,
)?;
page_vm.vm_mut().sync_live_document_style_sources();
let snapshot = page_vm
.vm_mut()
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(210, 100, 1.0))?
.expect("iframe canvas fixture must retain a root");
let image = moli_paint::raster_snapshot(&snapshot)?;
let pixel = |x: u32, y: u32| {
let index = ((y * image.width + x) * 4) as usize;
&image.rgba[index..index + 4]
};
assert_eq!(
pixel(50, 50),
[0, 128, 0, 255],
"a transparent child canvas must expose the iframe owner's background"
);
assert_eq!(
pixel(160, 50),
[0, 0, 255, 255],
"a child document's own canvas background must still cover the owner"
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("transparent iframe canvas fixture should run");
}
#[tokio::test(flavor = "current_thread")]
async fn screenshot_and_screencast_paint_fresh_canvas_2d_backing_stores() {
run_page_vm_async_test(async move {