libservo: Add a WebView::take_screenshot() API and use it for reftests

Co-authored-by: Delan Azabani <dazabani@igalia.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-26 09:24:24 +02:00
parent 92dd54b1ec
commit ebb12cb298
25 changed files with 481 additions and 414 deletions

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Ref, RefCell, RefMut};
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::mem;
@ -11,6 +11,7 @@ use std::rc::Rc;
use crossbeam_channel::Receiver;
use embedder_traits::webdriver::WebDriverSenders;
use image::{DynamicImage, ImageFormat};
use keyboard_types::ShortcutMatcher;
use log::{error, info};
use servo::base::generic_channel::GenericSender;
@ -31,7 +32,6 @@ use super::dialog::Dialog;
use super::gamepad::GamepadSupport;
use super::keyutils::CMD_OR_CONTROL;
use super::window_trait::WindowPortsMethods;
use crate::output_image::save_output_image_if_necessary;
use crate::prefs::ServoShellPreferences;
pub(crate) enum AppState {
@ -91,6 +91,10 @@ pub struct RunningAppStateInner {
/// List of webviews that have favicon textures which are not yet uploaded
/// to the GPU by egui.
pending_favicon_loads: Vec<WebViewId>,
/// Whether or not the application has achieved stable image output. This is used
/// for the `exit_after_stable_image` option.
acheived_stable_image: Rc<Cell<bool>>,
}
impl Drop for RunningAppState {
@ -123,6 +127,7 @@ impl RunningAppState {
need_repaint: false,
dialog_amount_changed: false,
pending_favicon_loads: Default::default(),
acheived_stable_image: Default::default(),
}),
}
}
@ -178,24 +183,12 @@ impl RunningAppState {
let Some(webview) = self.focused_webview() else {
return;
};
if !webview.paint() {
return;
}
// This needs to be done before presenting(), because `ReneringContext::read_to_image` reads
// from the back buffer.
save_output_image_if_necessary(
&self.servoshell_preferences,
&self.inner().window.rendering_context(),
);
webview.paint();
let mut inner_mut = self.inner_mut();
inner_mut.window.rendering_context().present();
inner_mut.need_repaint = false;
if self.servoshell_preferences.exit_after_stable_image {
self.servo().start_shutting_down();
}
}
/// Spins the internal application event loop.
@ -220,6 +213,12 @@ impl RunningAppState {
self.inner_mut().dialog_amount_changed = false;
if self.servoshell_preferences.exit_after_stable_image &&
self.inner().acheived_stable_image.get()
{
self.servo.start_shutting_down();
}
PumpResult::Continue {
need_update,
need_window_redraw,
@ -492,6 +491,44 @@ impl RunningAppState {
pub(crate) fn take_pending_favicon_loads(&self) -> Vec<WebViewId> {
mem::take(&mut self.inner_mut().pending_favicon_loads)
}
/// If we are exiting after acheiving a stable image or we want to save the display of the
/// [`WebView`] to an image file, request a screenshot of the [`WebView`].
fn maybe_request_screenshot(&self, webview: WebView) {
let output_path = self.servoshell_preferences.output_image_path.clone();
if !self.servoshell_preferences.exit_after_stable_image && output_path.is_none() {
return;
}
// Never request more than a single screenshot for now.
let acheived_stable_image = self.inner().acheived_stable_image.clone();
if acheived_stable_image.get() {
return;
}
webview.take_screenshot(move |image| {
acheived_stable_image.set(true);
let Some(output_path) = output_path else {
return;
};
let image = match image {
Ok(image) => image,
Err(error) => {
error!("Could not take screenshot: {error:?}");
return;
},
};
let image_format = ImageFormat::from_path(&output_path).unwrap_or(ImageFormat::Png);
if let Err(error) =
DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format)
{
error!("Failed to save screenshot: {error}.");
}
});
}
}
struct ServoShellServoDelegate;
@ -653,6 +690,7 @@ impl WebViewDelegate for RunningAppState {
{
let _ = sender.send(WebDriverLoadStatus::Complete);
}
self.maybe_request_screenshot(webview);
}
}

View file

@ -471,6 +471,11 @@ impl Window {
);
})
.shortcut(CMD_OR_CONTROL, 'Q', || state.servo().start_shutting_down())
.shortcut(Modifiers::empty(), 'P', || {
focused_webview.take_screenshot(|image| {
println!("Done taking screenshot: {:?}", image.is_ok());
});
})
.otherwise(|| handled = false);
handled
}

View file

@ -15,7 +15,6 @@ mod crash_handler;
pub(crate) mod desktop;
#[cfg(any(target_os = "android", target_env = "ohos"))]
mod egl;
mod output_image;
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
mod panic_hook;
mod parser;

View file

@ -1,39 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use euclid::Point2D;
use image::{DynamicImage, ImageFormat};
use log::error;
use servo::RenderingContext;
use servo::webrender_api::units::DeviceIntRect;
use crate::prefs::ServoShellPreferences;
/// This needs to be done before presenting(), because `ReneringContext::read_to_image` reads
/// from the back buffer. This does nothing if the preference `output_image_path` is not set.
pub(crate) fn save_output_image_if_necessary<T>(
prefs: &ServoShellPreferences,
rendering_context: &Rc<T>,
) where
T: RenderingContext + ?Sized,
{
let Some(output_path) = prefs.output_image_path.as_ref() else {
return;
};
let size = rendering_context.size2d().to_i32();
let viewport_rect = DeviceIntRect::from_origin_and_size(Point2D::origin(), size);
let Some(image) = rendering_context.read_to_image(viewport_rect) else {
error!("Failed to read output image.");
return;
};
let image_format = ImageFormat::from_path(output_path).unwrap_or(ImageFormat::Png);
if let Err(error) = DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format)
{
error!("Failed to save {output_path}: {error}.");
}
}

View file

@ -694,7 +694,6 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing
let opts = Opts {
debug: debug_options,
wait_for_stable_image: cmd_args.exit,
time_profiling: cmd_args.profile,
time_profiler_trace_path: cmd_args
.profiler_trace_path