Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TakeElementScreenshot WebDriver command #23943

Merged
merged 1 commit into from Aug 20, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

Some generated files are not rendered by default. Learn more.

@@ -14,7 +14,7 @@ use crate::CompositionPipeline;
use crate::SendableFrameTree;
use crossbeam_channel::Sender;
use embedder_traits::Cursor;
use euclid::{Point2D, Scale, Vector2D};
use euclid::{Point2D, Rect, Scale, Vector2D};
use gfx_traits::Epoch;
#[cfg(feature = "gl")]
use image::{DynamicImage, ImageFormat};
@@ -442,8 +442,8 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
self.touch_handler.on_event_processed(result);
},

(Msg::CreatePng(reply), ShutdownState::NotShuttingDown) => {
let res = self.composite_specific_target(CompositeTarget::WindowAndPng);
(Msg::CreatePng(rect, reply), ShutdownState::NotShuttingDown) => {
let res = self.composite_specific_target(CompositeTarget::WindowAndPng, rect);
if let Err(ref e) = res {
info!("Error retrieving PNG: {:?}", e);
}
@@ -1229,7 +1229,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {

pub fn composite(&mut self) {
let target = self.composite_target;
match self.composite_specific_target(target) {
match self.composite_specific_target(target, None) {
Ok(_) => {
if self.output_file.is_some() || self.exit_after_load {
println!("Shutting down the Constellation after generating an output file or exit flag specified");
@@ -1256,6 +1256,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
fn composite_specific_target(
&mut self,
target: CompositeTarget,
rect: Option<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, UnableToComposite> {
let size = self.embedder_coordinates.framebuffer.to_u32();

@@ -1347,15 +1348,33 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
}

let (x, y, width, height) = match rect {
Some(rect) => {
let rect = self.device_pixels_per_page_px().transform_rect(&rect);

let x = rect.origin.x as i32;
// We need to convert to the bottom-left origin coordinate
// system used by OpenGL
let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32;
This conversation was marked as resolved by jdm

This comment has been minimized.

@jdm

jdm Aug 10, 2019

Member

It's not clear to me why this is the right calculation to use here. Could you add a comment describing why it works?

This comment has been minimized.

@georgeroman

georgeroman Aug 10, 2019

Author Contributor

Yeah, sure, I added a comment.

let w = rect.size.width as u32;
let h = rect.size.height as u32;

(x, y, w, h)
},
None => (0, 0, size.width, size.height),
};

let rv = match target {
CompositeTarget::Window => None,
#[cfg(feature = "gl")]
CompositeTarget::WindowAndPng => {
let img = gl::draw_img(
&*self.window.gl(),
rt_info,
FramebufferUintLength::new(size.width),
FramebufferUintLength::new(size.height),
x,
y,
FramebufferUintLength::new(width),
FramebufferUintLength::new(height),
);
Some(Image {
width: img.width(),
@@ -1378,8 +1397,10 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
let img = gl::draw_img(
gl,
rt_info,
FramebufferUintLength::new(size.width),
FramebufferUintLength::new(size.height),
x,
y,
FramebufferUintLength::new(width),
FramebufferUintLength::new(height),
);
let dynamic_image = DynamicImage::ImageRgb8(img);
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::PNG)
@@ -8,6 +8,7 @@ use crate::compositor::CompositingReason;
use crate::SendableFrameTree;
use crossbeam_channel::{Receiver, Sender};
use embedder_traits::EventLoopWaker;
use euclid::Rect;
use gfx_traits::Epoch;
use ipc_channel::ipc::IpcSender;
use msg::constellation_msg::{PipelineId, TopLevelBrowsingContextId};
@@ -17,6 +18,7 @@ use profile_traits::time;
use script_traits::{AnimationState, ConstellationMsg, EventResult, MouseButton, MouseEventType};
use std::fmt::{Debug, Error, Formatter};
use style_traits::viewport::ViewportConstraints;
use style_traits::CSSPixel;
use webrender_api;
use webrender_api::units::{DeviceIntPoint, DeviceIntSize};
use webvr_traits::WebVRMainThreadHeartbeat;
@@ -80,7 +82,7 @@ pub enum Msg {
/// Script has handled a touch event, and either prevented or allowed default actions.
TouchEventProcessed(EventResult),
/// Composite to a PNG file and return the Image over a passed channel.
CreatePng(IpcSender<Option<Image>>),
CreatePng(Option<Rect<f32, CSSPixel>>, IpcSender<Option<Image>>),
/// Alerts the compositor that the viewport has been constrained in some manner
ViewportConstrained(PipelineId, ViewportConstraints),
/// A reply to the compositor asking if the output image is stable.
@@ -82,6 +82,8 @@ pub fn initialize_png(
pub fn draw_img(
gl: &dyn gl::Gl,
render_target_info: RenderTargetInfo,
x: i32,
y: i32,
width: FramebufferUintLength,
height: FramebufferUintLength,
) -> RgbImage {
@@ -96,8 +98,8 @@ pub fn draw_img(
gl.bind_vertex_array(0);

let mut pixels = gl.read_pixels(
0,
0,
x,
y,
width as gl::GLsizei,
height as gl::GLsizei,
gl::RGB,
@@ -3499,9 +3499,9 @@ where
y,
));
},
WebDriverCommandMsg::TakeScreenshot(_, reply) => {
WebDriverCommandMsg::TakeScreenshot(_, rect, reply) => {
self.compositor_proxy
.send(ToCompositorMsg::CreatePng(reply));
.send(ToCompositorMsg::CreatePng(rect, reply));
},
}
}
@@ -2198,6 +2198,14 @@ impl ScriptThread {
WebDriverScriptCommand::GetElementRect(node_id, reply) => {
webdriver_handlers::handle_get_rect(&*documents, pipeline_id, node_id, reply)
},
WebDriverScriptCommand::GetBoundingClientRect(node_id, reply) => {
webdriver_handlers::handle_get_bounding_client_rect(
&*documents,
pipeline_id,
node_id,
reply,
)
},
WebDriverScriptCommand::GetElementText(node_id, reply) => {
webdriver_handlers::handle_get_text(&*documents, pipeline_id, node_id, reply)
},
@@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
@@ -776,6 +777,30 @@ pub fn handle_get_rect(
.unwrap();
}

pub fn handle_get_bounding_client_rect(
documents: &Documents,
pipeline: PipelineId,
element_id: String,
reply: IpcSender<Result<Rect<f32>, ErrorStatus>>,
) {
reply
.send(
find_node_by_unique_id(documents, pipeline, element_id).and_then(|node| match node
.downcast::<Element>(
) {
Some(element) => {
let rect = element.GetBoundingClientRect();
Ok(Rect::new(
Point2D::new(rect.X() as f32, rect.Y() as f32),
Size2D::new(rect.Width() as f32, rect.Height() as f32),
))
},
None => Err(ErrorStatus::UnknownError),
}),
)
.unwrap();
}

pub fn handle_get_text(
documents: &Documents,
pipeline: PipelineId,
@@ -25,10 +25,7 @@ use canvas_traits::webgl::WebGLPipeline;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
use embedder_traits::{Cursor, EventLoopWaker};
use euclid::{
default::{Point2D, Rect},
Length, Scale, Size2D, Vector2D,
};
use euclid::{default::Point2D, Length, Rect, Scale, Size2D, UnknownUnit, Vector2D};
use gfx_traits::Epoch;
use http::HeaderMap;
use hyper::Method;
@@ -294,7 +291,7 @@ pub enum ConstellationControlMsg {
/// Sends a DOM event.
SendEvent(PipelineId, CompositorEvent),
/// Notifies script of the viewport.
Viewport(PipelineId, Rect<f32>),
Viewport(PipelineId, Rect<f32, UnknownUnit>),
/// Notifies script of a new set of scroll offsets.
SetScrollState(
PipelineId,
@@ -807,7 +804,11 @@ pub enum WebDriverCommandMsg {
IpcSender<WindowSizeData>,
),
/// Take a screenshot of the window.
TakeScreenshot(TopLevelBrowsingContextId, IpcSender<Option<Image>>),
TakeScreenshot(
TopLevelBrowsingContextId,
Option<Rect<f32, CSSPixel>>,
IpcSender<Option<Image>>,
),
}

/// Messages to the constellation.
@@ -74,6 +74,7 @@ pub enum WebDriverScriptCommand {
GetElementRect(String, IpcSender<Result<Rect<f64>, ErrorStatus>>),
GetElementTagName(String, IpcSender<Result<String, ErrorStatus>>),
GetElementText(String, IpcSender<Result<String, ErrorStatus>>),
GetBoundingClientRect(String, IpcSender<Result<Rect<f32>, ErrorStatus>>),
GetBrowsingContextId(
WebDriverFrameId,
IpcSender<Result<BrowsingContextId, ErrorStatus>>,
@@ -29,6 +29,7 @@ serde_json = "1"
script_traits = {path = "../script_traits"}
servo_config = {path = "../config"}
servo_url = {path = "../url"}
style_traits = {path = "../style_traits"}
url = "2.0"
uuid = {version = "0.7", features = ["v4"]}
webdriver = "0.40"
@@ -20,7 +20,7 @@ use crate::actions::InputSourceState;
use base64;
use capabilities::ServoCapabilities;
use crossbeam_channel::Sender;
use euclid::Size2D;
use euclid::{Rect, Size2D};
use hyper::Method;
use image::{DynamicImage, ImageFormat, RgbImage};
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@@ -44,6 +44,7 @@ use std::mem;
use std::net::{SocketAddr, SocketAddrV4};
use std::thread;
use std::time::Duration;
use style_traits::CSSPixel;
use uuid::Uuid;
use webdriver::actions::ActionSequence;
use webdriver::capabilities::{Capabilities, CapabilitiesMatching};
@@ -1445,16 +1446,20 @@ impl Handler {
Ok(WebDriverResponse::Void)
}

fn handle_take_screenshot(&self) -> WebDriverResult<WebDriverResponse> {
fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> {
let mut img = None;
let top_level_id = self.session()?.top_level_browsing_context_id;

let interval = 1000;
let iterations = 30_000 / interval;
let iterations = 30000 / interval;

for _ in 0..iterations {
let (sender, receiver) = ipc::channel().unwrap();
let cmd_msg = WebDriverCommandMsg::TakeScreenshot(top_level_id, sender);

let cmd_msg = WebDriverCommandMsg::TakeScreenshot(
self.session()?.top_level_browsing_context_id,
rect,
sender,
);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
@@ -1464,7 +1469,7 @@ impl Handler {
break;
};

thread::sleep(Duration::from_millis(interval))
thread::sleep(Duration::from_millis(interval));
}

let img = match img {
@@ -1483,19 +1488,50 @@ impl Handler {
PixelFormat::RGB8,
"Unexpected screenshot pixel format"
);
let rgb = RgbImage::from_raw(img.width, img.height, img.bytes.to_vec()).unwrap();

let rgb = RgbImage::from_raw(img.width, img.height, img.bytes.to_vec()).unwrap();
let mut png_data = Vec::new();
DynamicImage::ImageRgb8(rgb)
.write_to(&mut png_data, ImageFormat::PNG)
.unwrap();

let encoded = base64::encode(&png_data);
Ok(base64::encode(&png_data))
}

fn handle_take_screenshot(&self) -> WebDriverResult<WebDriverResponse> {
let encoded = self.take_screenshot(None)?;

Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(encoded)?,
)))
}

fn handle_take_element_screenshot(
&self,
element: &WebElement,
) -> WebDriverResult<WebDriverResponse> {
let (sender, receiver) = ipc::channel().unwrap();

let command = WebDriverScriptCommand::GetBoundingClientRect(element.to_string(), sender);
self.browsing_context_script_command(command)?;

match receiver.recv().unwrap() {
Ok(rect) => {
let encoded = self.take_screenshot(Some(Rect::from_untyped(&rect)))?;

Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(encoded)?,
)))
},
Err(_) => {
return Err(WebDriverError::new(
ErrorStatus::StaleElementReference,
"Element not found",
));
},
}
}

fn handle_get_prefs(
&self,
parameters: &GetPrefsParameters,
@@ -1635,6 +1671,9 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler {
WebDriverCommand::GetTimeouts => self.handle_get_timeouts(),
WebDriverCommand::SetTimeouts(ref x) => self.handle_set_timeouts(x),
WebDriverCommand::TakeScreenshot => self.handle_take_screenshot(),
WebDriverCommand::TakeElementScreenshot(ref x) => {
self.handle_take_element_screenshot(x)
},
WebDriverCommand::Extension(ref extension) => match *extension {
ServoExtensionCommand::GetPrefs(ref x) => self.handle_get_prefs(x),
ServoExtensionCommand::SetPrefs(ref x) => self.handle_set_prefs(x),
@@ -1,2 +1,7 @@
[screenshot.py]
disabled: Unimplemented WebDriver command
[test_no_browsing_context]
expected: ERROR

[test_format_and_dimensions]
This conversation was marked as resolved by jdm

This comment has been minimized.

@jdm

jdm Aug 10, 2019

Member

It would be good to understand what fails in this test.

This comment has been minimized.

@georgeroman

georgeroman Aug 10, 2019

Author Contributor

The test uses element_rect which in turn uses ExecuteScript to get an object containing the element's dimensions. Since we can't have objects returned from ExecuteScript yet, the test fails.

expected: FAIL

ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.