diff --git a/moli-layout/src/capture.rs b/moli-layout/src/capture.rs index fe3e29bfcb..36c275262d 100644 --- a/moli-layout/src/capture.rs +++ b/moli-layout/src/capture.rs @@ -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, /// 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), } diff --git a/moli-layout/src/paint/mod.rs b/moli-layout/src/paint/mod.rs index 1b4b7cd993..7ba3791e86 100644 --- a/moli-layout/src/paint/mod.rs +++ b/moli-layout/src/paint/mod.rs @@ -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( world: &LayoutWorld, root: LayoutBoxId, + base_background_color: PaintColor, ) -> (PaintColor, Option) 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 { diff --git a/moli-layout/src/snapshot.rs b/moli-layout/src/snapshot.rs index 25ed191d18..f4fc1f9500 100644 --- a/moli-layout/src/snapshot.rs +++ b/moli-layout/src/snapshot.rs @@ -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, diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs index c7d9f36026..ab1b7bd179 100644 --- a/moli-renderer-v8/src/layout_renderer/mod.rs +++ b/moli-renderer-v8/src/layout_renderer/mod.rs @@ -113,6 +113,7 @@ impl EmbeddedFrameRenderer 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) diff --git a/moli-renderer-v8/src/runtime/page_screenshot.rs b/moli-renderer-v8/src/runtime/page_screenshot.rs index 804d03cb2c..4e89afb8e2 100644 --- a/moli-renderer-v8/src/runtime/page_screenshot.rs +++ b/moli-renderer-v8/src/runtime/page_screenshot.rs @@ -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, }) diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index 325ffa385f..753ef010b2 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -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 = ` + +`; +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 {