Support transparent screenshot surfaces

This commit is contained in:
SGavrl committed 2026-08-04 10:41:57 +00:00
1 parent 79dba60fae
commit add39f489e
7 files changed
+607 -35

No files matched your search

+22 -3
View File
@@ -228,6 +228,9 @@ pub struct Page {
/// CSSOM stay in CSS pixels; Emulation.setDeviceMetricsOverride owns this
/// independent raster scale.
pub device_scale_factor: f32,
/// DevTools override for the compositor's base surface. It is page-owned,
/// so it survives document navigation without leaking to other targets.
default_background_color_override: Option<[u8; 4]>,
/// WHATWG canonical name of the current document's character encoding
/// (e.g. "UTF-8", "EUC-JP"), detected when the response body is decoded.
/// Exposed to JS as `document.characterSet` and used for the URL query
@@ -727,6 +730,7 @@ impl Page {
referrer: String::new(),
viewport: (1280.0, 720.0),
device_scale_factor: 1.0,
default_background_color_override: None,
encoding: "UTF-8".to_string(),
document_timeline_origin: std::time::Instant::now(),
history: Vec::new(),
@@ -800,6 +804,16 @@ impl Page {
}
}
pub fn set_default_background_color_override(&mut self, color: Option<[u8; 4]>) {
self.default_background_color_override = color;
}
#[cfg(feature = "render")]
fn capture_surface_color(&self) -> [u8; 4] {
self.default_background_color_override
.unwrap_or([255, 255, 255, 255])
}
async fn do_fetch(&self, url: &Url) -> Result<Response, ObscuraNetError> {
#[cfg(feature = "stealth")]
if let Some(ref stealth) = self.stealth_client {
@@ -2672,7 +2686,11 @@ impl Page {
if !js.set_animation_sample(animation_sample) {
return None;
}
if let Some(png) = js.screenshot_prepared(viewport, base_url) {
if let Some(png) = js.screenshot_prepared_with_surface_color(
viewport,
base_url,
self.capture_surface_color(),
) {
return Some(png);
}
}
@@ -2684,12 +2702,13 @@ impl Page {
.map(|js| js.scroll_offset())
.unwrap_or((0.0, 0.0));
self.with_dom(|dom| {
obscura_js::screenshot_png_scrolled_at_animation_time(
obscura_js::screenshot_png_scrolled_at_animation_time_with_surface_color(
dom,
viewport,
base_url,
scroll,
animation_sample.time,
self.capture_surface_color(),
)
})
.flatten()
@@ -2734,7 +2753,7 @@ impl Page {
if !js.set_animation_sample(animation_sample) {
return Err(obscura_js::CaptureError::PaintFailed);
}
js.screenshot_prepared_region(region)
js.screenshot_prepared_region_with_surface_color(region, self.capture_surface_color())
}
/// Scrollable document dimensions from the retained render layout. Unlike
@@ -18,6 +18,40 @@ fn metric_dimension(params: &Value, name: &str) -> Result<u32, String> {
Ok(value as u32)
}
fn default_background_color(params: &Value) -> Result<Option<[u8; 4]>, String> {
let Some(color) = params.get("color") else {
return Ok(None);
};
let color = color.as_object().ok_or(
"Emulation.setDefaultBackgroundColorOverride color must be an RGBA object",
)?;
let channel = |name: &str| -> Result<u8, String> {
let value = color.get(name).and_then(Value::as_i64).ok_or_else(|| {
format!(
"Emulation.setDefaultBackgroundColorOverride requires integer color.{name}"
)
})?;
Ok(value.clamp(0, 255) as u8)
};
let alpha = match color.get("a") {
Some(value) => value.as_f64().ok_or(
"Emulation.setDefaultBackgroundColorOverride color.a must be a number",
)?,
None => 1.0,
};
if !alpha.is_finite() {
return Err(
"Emulation.setDefaultBackgroundColorOverride color.a must be finite".to_string(),
);
}
Ok(Some([
channel("r")?,
channel("g")?,
channel("b")?,
((alpha as f32).clamp(0.0, 1.0) * 255.0).round() as u8,
]))
}
pub async fn handle(
method: &str,
params: &Value,
@@ -64,6 +98,14 @@ pub async fn handle(
page.set_device_scale_factor(1.0);
Ok(json!({}))
}
"setDefaultBackgroundColorOverride" => {
let color = default_background_color(params)?;
let page = ctx
.get_session_page_mut(session_id)
.ok_or("No page for session")?;
page.set_default_background_color_override(color);
Ok(json!({}))
}
// Touch emulation does not affect layout yet, but acknowledging it is
// compatible with clients that pair it with a metrics override.
"setTouchEmulationEnabled" => Ok(json!({})),
@@ -194,4 +236,46 @@ mod tests {
assert_eq!(page.viewport, (1280.0, 720.0));
assert_eq!(page.device_scale_factor, 1.0);
}
#[test]
fn default_background_color_matches_blink_defaults_rounding_and_clamping() {
assert_eq!(default_background_color(&json!({})), Ok(None));
assert_eq!(
default_background_color(&json!({"color": {"r": 1, "g": 2, "b": 3}})),
Ok(Some([1, 2, 3, 255]))
);
assert_eq!(
default_background_color(&json!({
"color": {"r": -20, "g": 400, "b": 30, "a": 16.0 / 255.0}
})),
Ok(Some([0, 255, 30, 16]))
);
assert_eq!(
default_background_color(&json!({
"color": {"r": 1, "g": 2, "b": 3, "a": 2}
})),
Ok(Some([1, 2, 3, 255]))
);
assert_eq!(
default_background_color(&json!({
"color": {"r": 1, "g": 2, "b": 3, "a": -1}
})),
Ok(Some([1, 2, 3, 0]))
);
}
#[test]
fn default_background_color_rejects_malformed_rgba() {
for params in [
json!({"color": null}),
json!({"color": {"g": 2, "b": 3}}),
json!({"color": {"r": 1.5, "g": 2, "b": 3}}),
json!({"color": {"r": 1, "g": 2, "b": 3, "a": "opaque"}}),
] {
assert!(
default_background_color(&params).is_err(),
"must reject {params}"
);
}
}
}
+285 -21
View File
@@ -702,6 +702,7 @@ pub(crate) fn command_can_change_screencast_frame(method: &str) -> bool {
| "Input.dispatchTouchEvent"
| "Emulation.setDeviceMetricsOverride"
| "Emulation.clearDeviceMetricsOverride"
| "Emulation.setDefaultBackgroundColorOverride"
| "DOM.setAttributeValue"
| "DOM.removeNode"
| "DOM.focus"
@@ -1428,20 +1429,6 @@ pub async fn handle(
};
let region = if let Some(clip) = options.clip {
let relative_x = clip.x - scroll.0;
let relative_y = clip.y - scroll.1;
let epsilon = 1e-6;
if !options.capture_beyond_viewport
&& (relative_x < -epsilon
|| relative_y < -epsilon
|| relative_x + clip.width > f64::from(viewport.0) + epsilon
|| relative_y + clip.height > f64::from(viewport.1) + epsilon)
{
return Err(format!(
"Page.captureScreenshot clip lies outside the current viewport surface while captureBeyondViewport=false (visible page rect: x={} y={} width={} height={})",
scroll.0, scroll.1, viewport.0, viewport.1
));
}
Some(chromium_clip_region(clip, device_scale_factor)?)
} else if let Some(content_size) = full_page_size {
Some(obscura_browser::CaptureRegion::new(
@@ -1682,6 +1669,273 @@ mod tests {
(ctx, session)
}
#[cfg(feature = "render")]
async fn transparent_surface_fixture(
height: u32,
) -> (CdpContext, Option<String>) {
let mut ctx = CdpContext::new();
let page_id = ctx.create_page();
let session_id = format!("{page_id}-session");
ctx.sessions.insert(session_id.clone(), page_id);
let session = Some(session_id);
ctx.get_session_page_mut(&session)
.expect("page")
.set_viewport((100.0, 80.0));
handle(
"navigate",
&json!({
"url": format!("data:text/html,<html style='margin:0;background:transparent'><body style='margin:0;height:{height}px;background:transparent'></body></html>"),
"waitUntil": "load",
}),
&mut ctx,
&session,
)
.await
.expect("navigate transparent surface fixture");
(ctx, session)
}
#[cfg(feature = "render")]
#[tokio::test]
async fn default_background_override_matches_chromium_across_capture_state() {
let (mut ctx, session) = transparent_surface_fixture(160).await;
let (_, default_raster) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("default white capture"),
);
assert_eq!(default_raster.get_pixel(50, 40).0, [255, 255, 255, 255]);
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 0, "g": 0, "b": 255, "a": 1}}),
&mut ctx,
&session,
)
.await
.expect("opaque blue override");
let (_, blue) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("blue capture"),
);
assert!(blue.pixels().all(|pixel| pixel.0 == [0, 0, 255, 255]));
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 0, "g": 0, "b": 0, "a": 0}}),
&mut ctx,
&session,
)
.await
.expect("transparent override");
let (_, transparent) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("transparent capture"),
);
assert!(transparent.pixels().all(|pixel| pixel.0 == [0, 0, 0, 0]));
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 255, "g": 0, "b": 0, "a": 16.0 / 255.0}}),
&mut ctx,
&session,
)
.await
.expect("semi-transparent override");
crate::domains::emulation::handle(
"setDeviceMetricsOverride",
&json!({"width": 40, "height": 30, "deviceScaleFactor": 2}),
&mut ctx,
&session,
)
.await
.expect("metrics override");
let (_, semi) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("semi-transparent metrics capture"),
);
assert_eq!(semi.dimensions(), (80, 60));
assert!(semi.pixels().all(|pixel| pixel.0 == [255, 0, 0, 16]));
handle(
"navigate",
&json!({
"url": "data:text/html,<html style='margin:0;background:transparent'><body style='margin:0;background:transparent'></body></html>",
"waitUntil": "load",
}),
&mut ctx,
&session,
)
.await
.expect("navigate with live override");
let (_, after_navigation) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("capture after navigation"),
);
assert_eq!(after_navigation.get_pixel(20, 15).0, [255, 0, 0, 16]);
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({}),
&mut ctx,
&session,
)
.await
.expect("clear override");
let (_, cleared) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &session)
.await
.expect("capture after clear"),
);
assert_eq!(cleared.get_pixel(20, 15).0, [255, 255, 255, 255]);
}
#[cfg(feature = "render")]
#[tokio::test]
async fn default_background_override_is_target_isolated() {
let (mut ctx, first_session) = transparent_surface_fixture(80).await;
let second_page_id = ctx.create_page();
let second_session_id = format!("{second_page_id}-session");
ctx.sessions
.insert(second_session_id.clone(), second_page_id);
let second_session = Some(second_session_id);
ctx.get_session_page_mut(&second_session)
.expect("second page")
.set_viewport((100.0, 80.0));
handle(
"navigate",
&json!({
"url": "data:text/html,<html style='margin:0;background:transparent'><body style='margin:0;background:transparent'></body></html>",
"waitUntil": "load",
}),
&mut ctx,
&second_session,
)
.await
.expect("navigate second page");
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 20, "g": 40, "b": 60}}),
&mut ctx,
&first_session,
)
.await
.expect("first-page override");
let (_, first) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &first_session)
.await
.expect("first-page capture"),
);
let (_, second) = decode_capture(
&handle("captureScreenshot", &json!({}), &mut ctx, &second_session)
.await
.expect("second-page capture"),
);
assert_eq!(first.get_pixel(50, 40).0, [20, 40, 60, 255]);
assert_eq!(second.get_pixel(50, 40).0, [255, 255, 255, 255]);
}
#[cfg(feature = "render")]
#[tokio::test]
async fn default_background_override_covers_clips_full_page_and_screencast_damage() {
let (mut ctx, session) = transparent_surface_fixture(160).await;
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 7, "g": 19, "b": 31, "a": 1}}),
&mut ctx,
&session,
)
.await
.expect("surface override");
for clip in [
json!({"x": 90, "y": 0, "width": 20, "height": 20, "scale": 1}),
json!({"x": 0, "y": 120, "width": 20, "height": 20, "scale": 1}),
] {
let (_, raster) = decode_capture(
&handle(
"captureScreenshot",
&json!({"captureBeyondViewport": false, "clip": clip}),
&mut ctx,
&session,
)
.await
.expect("off-viewport clip with default false"),
);
assert_eq!(raster.dimensions(), (20, 20));
assert!(raster
.pixels()
.all(|pixel| pixel.0 == [7, 19, 31, 255]));
}
ctx.get_session_page_mut(&session)
.expect("page")
.evaluate("window.scrollTo(0, 80)");
let (_, above_scroll) = decode_capture(
&handle(
"captureScreenshot",
&json!({
"clip": {"x": 0, "y": 0, "width": 20, "height": 20, "scale": 1}
}),
&mut ctx,
&session,
)
.await
.expect("document clip above live scroll"),
);
assert_eq!(above_scroll.get_pixel(10, 10).0, [7, 19, 31, 255]);
assert_eq!(
ctx.get_session_page(&session)
.expect("page")
.screenshot_scroll_offset(),
(0.0, 80.0),
"clip capture must not mutate live scroll"
);
let (_, full_page) = decode_capture(
&handle(
"captureScreenshot",
&json!({"captureBeyondViewport": true}),
&mut ctx,
&session,
)
.await
.expect("full-page override capture"),
);
assert_eq!(full_page.dimensions(), (100, 160));
assert_eq!(full_page.get_pixel(50, 140).0, [7, 19, 31, 255]);
ctx.pending_events.clear();
handle("startScreencast", &json!({}), &mut ctx, &session)
.await
.expect("start screencast");
ctx.pending_events.clear();
let response = crate::dispatch::dispatch(
&crate::types::CdpRequest {
id: 99,
method: "Emulation.setDefaultBackgroundColorOverride".to_string(),
params: json!({"color": {"r": 90, "g": 80, "b": 70, "a": 1}}),
session_id: session.clone(),
},
&mut ctx,
)
.await;
assert!(response.error.is_none(), "override response: {response:?}");
let event = ctx
.pending_events
.iter()
.find(|event| event.method == "Page.screencastFrame")
.expect("background override must damage active screencast");
let (_, frame) = decode_capture(&event.params);
assert_eq!(frame.get_pixel(50, 40).0, [90, 80, 70, 255]);
}
#[cfg(feature = "render")]
#[tokio::test]
async fn screencast_initial_frame_metadata_and_encoding_match_options() {
@@ -2122,7 +2376,7 @@ mod tests {
handle(
"navigate",
&json!({
"url": "data:text/html,<html style='margin:0'><body style='margin:0;width:1000px;height:17000px;background:rgb(180,20,30)'><div style='position:absolute;left:0;top:8490px;width:1000px;height:30px;background:rgb(20,160,40)'></div><div style='position:absolute;left:0;top:16950px;width:1000px;height:50px;background:rgb(20,40,200)'></div><div style='position:fixed;z-index:2;left:0;top:10px;width:20px;height:20px;background:rgb(240,220,10)'></div></body></html>",
"url": "data:text/html,<html style='margin:0;background:transparent'><body style='margin:0;width:1000px;height:17000px;background:transparent'><div style='position:absolute;left:0;top:8490px;width:1000px;height:30px;background:rgb(20,160,40)'></div><div style='position:absolute;left:0;top:16950px;width:1000px;height:50px;background:rgb(20,40,200)'></div><div style='position:fixed;z-index:2;left:0;top:10px;width:20px;height:20px;background:rgb(240,220,10)'></div></body></html>",
"waitUntil": "load",
}),
&mut ctx,
@@ -2130,6 +2384,14 @@ mod tests {
)
.await
.expect("navigate long screenshot fixture");
crate::domains::emulation::handle(
"setDefaultBackgroundColorOverride",
&json!({"color": {"r": 180, "g": 20, "b": 30, "a": 1}}),
&mut ctx,
&session,
)
.await
.expect("long-page surface override");
ctx.get_session_page_mut(&session)
.expect("page")
.evaluate("window.scrollTo(0, 8000)");
@@ -2376,7 +2638,7 @@ mod tests {
#[cfg(feature = "render")]
#[tokio::test]
async fn capture_screenshot_supports_full_page_but_rejects_unrepresented_surfaces() {
async fn capture_screenshot_supports_full_page_and_off_viewport_clips() {
let (mut ctx, session) = screenshot_fixture().await;
ctx.get_session_page_mut(&session).expect("page").evaluate(
"Object.defineProperty(globalThis,'innerWidth',{value:4096,configurable:true});\
@@ -2417,11 +2679,13 @@ mod tests {
&session,
)
.await
.expect_err("partial off-viewport clip is unsupported");
assert!(
off_viewport.contains("outside the current viewport surface"),
"{off_viewport}"
);
.expect("Chromium accepts a partial off-viewport surface clip");
let (_, raster) = decode_capture(&off_viewport);
assert_eq!(raster.dimensions(), (20, 20));
assert_eq!(raster.get_pixel(5, 10).0, [0, 0, 255, 255]);
// TODO(canvas-background-transfer): Chromium propagates the body's red
// canvas background into the half beyond the 100px document box. This
// regression covers acceptance and the represented clip content.
}
#[cfg(feature = "render")]
+1
View File
@@ -14,6 +14,7 @@ pub use v8_flags::set_v8_flags;
#[cfg(feature = "render")]
pub use obscura_render::{
screenshot_png, screenshot_png_scrolled, screenshot_png_scrolled_at_animation_time,
screenshot_png_scrolled_at_animation_time_with_surface_color,
validate_capture_region, AnimationSample, AnimationSampleMode, AnimationSampleTime,
CaptureError, CaptureRegion, ImageRequestProfile,
MAX_CAPTURE_DIMENSION, MAX_CAPTURE_PIXELS,
+50 -2
View File
@@ -534,6 +534,20 @@ impl ObscuraJsRuntime {
&self,
viewport: (f32, f32),
base_url: Option<&str>,
) -> Option<Vec<u8>> {
self.screenshot_prepared_with_surface_color(
viewport,
base_url,
[255, 255, 255, 255],
)
}
#[cfg(feature = "render")]
pub fn screenshot_prepared_with_surface_color(
&self,
viewport: (f32, f32),
base_url: Option<&str>,
surface_color: [u8; 4],
) -> Option<Vec<u8>> {
let mut state = self.state.borrow_mut();
let effective_base = document_base_url(&state);
@@ -550,11 +564,12 @@ impl ObscuraJsRuntime {
..
} = state;
let (_, scroll) = resolved_scroll.as_ref()?;
obscura_render::screenshot_prepared_with_scroll(
obscura_render::screenshot_prepared_with_scroll_and_surface_color(
dom.as_ref()?,
prepared_render.as_mut()?,
render_resources,
scroll,
surface_color,
)
})
}
@@ -567,7 +582,40 @@ impl ObscuraJsRuntime {
&self,
region: obscura_render::CaptureRegion,
) -> Result<Vec<u8>, obscura_render::CaptureError> {
self.screenshot_prepared_region_with_backgrounds(region, true)
self.screenshot_prepared_region_with_surface_color(region, [255, 255, 255, 255])
}
#[cfg(feature = "render")]
pub fn screenshot_prepared_region_with_surface_color(
&self,
region: obscura_render::CaptureRegion,
surface_color: [u8; 4],
) -> Result<Vec<u8>, obscura_render::CaptureError> {
let mut state = self.state.borrow_mut();
with_sync_render_loading_disabled(&mut state, |state| {
ensure_resolved_scroll(state).ok_or(obscura_render::CaptureError::PaintFailed)?;
let ObscuraState {
dom,
prepared_render,
render_resources,
resolved_scroll,
..
} = state;
let (_, scroll) = resolved_scroll
.as_ref()
.ok_or(obscura_render::CaptureError::PaintFailed)?;
obscura_render::screenshot_prepared_region_with_scroll_and_surface_color(
dom.as_ref()
.ok_or(obscura_render::CaptureError::PaintFailed)?,
prepared_render
.as_mut()
.ok_or(obscura_render::CaptureError::PaintFailed)?,
render_resources,
scroll,
region,
surface_color,
)
})
}
/// Capture a document-space rectangle with the PDF print-background
+8 -3
View File
@@ -98,20 +98,25 @@ mod paint;
#[cfg(feature = "paint")]
pub use paint::{
image_intrinsic_dimensions, paint_dom, paint_dom_scrolled,
paint_dom_scrolled_at_animation_time, paint_prepared,
paint_prepared_region_with_scroll,
paint_prepared_with_scroll, prepare_dom, prepare_dom_at_animation_time,
paint_dom_scrolled_at_animation_time,
paint_dom_scrolled_at_animation_time_with_surface_color, paint_prepared,
paint_prepared_region_with_scroll, paint_prepared_region_with_scroll_and_surface_color,
paint_prepared_with_scroll, paint_prepared_with_scroll_and_surface_color, prepare_dom,
prepare_dom_at_animation_time,
prepare_dom_with_dynamic_fonts, prepare_dom_with_dynamic_fonts_at_animation_time,
prepare_dom_with_dynamic_fonts_and_stylesheet_cache,
prepare_dom_with_dynamic_fonts_and_stylesheet_cache_at_animation_time,
prepare_dom_with_dynamic_fonts_and_stylesheet_cache_with_animation_state,
screenshot_png, screenshot_png_scrolled, screenshot_png_scrolled_at_animation_time,
screenshot_png_scrolled_at_animation_time_with_surface_color,
prepare_dom_with_retained_attribute_styles, prepare_dom_with_retained_styles,
prepare_dom_with_retained_styles_at_animation_time,
prepare_dom_with_retained_styles_with_animation_state,
screenshot_prepared,
screenshot_prepared_region_with_scroll,
screenshot_prepared_region_with_scroll_and_surface_color,
screenshot_prepared_region_with_scroll_and_backgrounds, screenshot_prepared_with_scroll,
screenshot_prepared_with_scroll_and_surface_color,
validate_capture_region, CaptureError, CaptureRegion, DynamicFontFace, ElementScrollMetrics,
ImageRequestProfile, PreparedRender, RenderResourceCache, RenderResourceLoader,
ResolvedScrollState, SelectedImage,
+157 -6
View File
@@ -1826,6 +1826,24 @@ pub fn paint_dom_scrolled_at_animation_time(
base_url: Option<&str>,
scroll: (f32, f32),
animation_sample_time: crate::AnimationSampleTime,
) -> Option<Pixmap> {
paint_dom_scrolled_at_animation_time_with_surface_color(
tree,
viewport,
base_url,
scroll,
animation_sample_time,
[255, 255, 255, 255],
)
}
pub fn paint_dom_scrolled_at_animation_time_with_surface_color(
tree: &DomTree,
viewport: (f32, f32),
base_url: Option<&str>,
scroll: (f32, f32),
animation_sample_time: crate::AnimationSampleTime,
surface_color: [u8; 4],
) -> Option<Pixmap> {
let mut resources = RenderResourceCache::default();
let mut prepared = prepare_dom_at_animation_time(
@@ -1835,7 +1853,7 @@ pub fn paint_dom_scrolled_at_animation_time(
&mut resources,
animation_sample_time,
)?;
paint_prepared(tree, &mut prepared, &mut resources, scroll)
paint_prepared_with_surface_color(tree, &mut prepared, &mut resources, scroll, surface_color)
}
/// Resolve image candidates and web fonts, then create the single final layout
@@ -2246,6 +2264,16 @@ pub fn paint_prepared(
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: (f32, f32),
) -> Option<Pixmap> {
paint_prepared_with_surface_color(tree, prepared, resources, scroll, [255, 255, 255, 255])
}
fn paint_prepared_with_surface_color(
tree: &DomTree,
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: (f32, f32),
surface_color: [u8; 4],
) -> Option<Pixmap> {
validate_capture_region(CaptureRegion::new(
scroll.0,
@@ -2257,7 +2285,12 @@ pub fn paint_prepared(
.ok()?;
let (w, h) = (prepared.viewport.0 as u32, prepared.viewport.1 as u32);
let mut pixmap = Pixmap::new(w, h)?;
pixmap.fill(Color::WHITE);
pixmap.fill(Color::from_rgba8(
surface_color[0],
surface_color[1],
surface_color[2],
surface_color[3],
));
paint_laid_dom_scrolled(
tree,
prepared.viewport,
@@ -2291,6 +2324,22 @@ pub fn paint_prepared_with_scroll(
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: &ResolvedScrollState,
) -> Option<Pixmap> {
paint_prepared_with_scroll_and_surface_color(
tree,
prepared,
resources,
scroll,
[255, 255, 255, 255],
)
}
pub fn paint_prepared_with_scroll_and_surface_color(
tree: &DomTree,
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: &ResolvedScrollState,
surface_color: [u8; 4],
) -> Option<Pixmap> {
validate_capture_region(CaptureRegion::new(
scroll.root_offset().0,
@@ -2302,7 +2351,12 @@ pub fn paint_prepared_with_scroll(
.ok()?;
let (w, h) = (prepared.viewport.0 as u32, prepared.viewport.1 as u32);
let mut pixmap = Pixmap::new(w, h)?;
pixmap.fill(Color::WHITE);
pixmap.fill(Color::from_rgba8(
surface_color[0],
surface_color[1],
surface_color[2],
surface_color[3],
));
paint_laid_dom_scrolled(
tree,
prepared.viewport,
@@ -2340,7 +2394,33 @@ pub fn paint_prepared_region_with_scroll(
scroll: &ResolvedScrollState,
region: CaptureRegion,
) -> Result<Pixmap, CaptureError> {
paint_prepared_region_with_scroll_policy(tree, prepared, resources, scroll, region, false)
paint_prepared_region_with_scroll_and_surface_color(
tree,
prepared,
resources,
scroll,
region,
[255, 255, 255, 255],
)
}
pub fn paint_prepared_region_with_scroll_and_surface_color(
tree: &DomTree,
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: &ResolvedScrollState,
region: CaptureRegion,
surface_color: [u8; 4],
) -> Result<Pixmap, CaptureError> {
paint_prepared_region_with_scroll_policy(
tree,
prepared,
resources,
scroll,
region,
false,
surface_color,
)
}
fn paint_prepared_region_with_scroll_with_print_economy(
@@ -2350,7 +2430,15 @@ fn paint_prepared_region_with_scroll_with_print_economy(
scroll: &ResolvedScrollState,
region: CaptureRegion,
) -> Result<Pixmap, CaptureError> {
paint_prepared_region_with_scroll_policy(tree, prepared, resources, scroll, region, true)
paint_prepared_region_with_scroll_policy(
tree,
prepared,
resources,
scroll,
region,
true,
[255, 255, 255, 255],
)
}
fn paint_prepared_region_with_scroll_policy(
@@ -2360,6 +2448,7 @@ fn paint_prepared_region_with_scroll_policy(
scroll: &ResolvedScrollState,
region: CaptureRegion,
print_economy: bool,
surface_color: [u8; 4],
) -> Result<Pixmap, CaptureError> {
let (native_width, native_height, output_width, output_height) =
checked_capture_dimensions(region)?;
@@ -2377,7 +2466,12 @@ fn paint_prepared_region_with_scroll_policy(
};
let mut pixmap =
Pixmap::new(paint_width, paint_height).ok_or(CaptureError::AllocationLimitExceeded)?;
pixmap.fill(Color::WHITE);
pixmap.fill(Color::from_rgba8(
surface_color[0],
surface_color[1],
surface_color[2],
surface_color[3],
));
// Resolved node movement already contains `-root_scroll` for ordinary
// document content and zero root movement for fixed content. Adding the
@@ -5278,6 +5372,25 @@ pub fn screenshot_png_scrolled_at_animation_time(
.and_then(|pixmap| pixmap.encode_png().ok())
}
pub fn screenshot_png_scrolled_at_animation_time_with_surface_color(
tree: &DomTree,
viewport: (f32, f32),
base_url: Option<&str>,
scroll: (f32, f32),
animation_sample_time: crate::AnimationSampleTime,
surface_color: [u8; 4],
) -> Option<Vec<u8>> {
paint_dom_scrolled_at_animation_time_with_surface_color(
tree,
viewport,
base_url,
scroll,
animation_sample_time,
surface_color,
)
.and_then(|pixmap| pixmap.encode_png().ok())
}
/// PNG convenience wrapper for a retained resource-aware layout.
pub fn screenshot_prepared(
tree: &DomTree,
@@ -5301,6 +5414,24 @@ pub fn screenshot_prepared_with_scroll(
.ok()
}
pub fn screenshot_prepared_with_scroll_and_surface_color(
tree: &DomTree,
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: &ResolvedScrollState,
surface_color: [u8; 4],
) -> Option<Vec<u8>> {
paint_prepared_with_scroll_and_surface_color(
tree,
prepared,
resources,
scroll,
surface_color,
)?
.encode_png()
.ok()
}
/// PNG convenience wrapper for [`paint_prepared_region_with_scroll`].
pub fn screenshot_prepared_region_with_scroll(
tree: &DomTree,
@@ -5314,6 +5445,26 @@ pub fn screenshot_prepared_region_with_scroll(
.map_err(|_| CaptureError::EncodeFailed)
}
pub fn screenshot_prepared_region_with_scroll_and_surface_color(
tree: &DomTree,
prepared: &mut PreparedRender,
resources: &mut RenderResourceCache,
scroll: &ResolvedScrollState,
region: CaptureRegion,
surface_color: [u8; 4],
) -> Result<Vec<u8>, CaptureError> {
paint_prepared_region_with_scroll_and_surface_color(
tree,
prepared,
resources,
scroll,
region,
surface_color,
)?
.encode_png()
.map_err(|_| CaptureError::EncodeFailed)
}
/// Capture a retained region using the PDF print-background policy. Ordinary
/// screenshots pass `true`. When false, authored fills are replaced with the
/// browser print-economy white fill and light text is darkened for legibility;