diff --git a/Cargo.toml b/Cargo.toml index c9ae89da4..4f0e1c0cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,8 @@ os-webview = [ "dep:gtk", "soup3", "x11-dl", - "gdkx11" + "gdkx11", + "cairo-rs" ] tracing = [ "dep:tracing" ] @@ -59,6 +60,7 @@ javascriptcore-rs = { version = "=1.1.2", features = [ "v2_28" ], optional = tru webkit2gtk = { version = "=2.0.1", features = [ "v2_38" ], optional = true } webkit2gtk-sys = { version = "=2.0.1", optional = true } gtk = { version = "0.18", optional = true } +cairo-rs = { version = "0.18", features = ["png"], optional = true } soup3 = { version = "0.5", optional = true } x11-dl = { version = "2.21", optional = true } gdkx11 = { version = "0.18", optional = true } diff --git a/examples/screenshot.rs b/examples/screenshot.rs new file mode 100644 index 000000000..4c7617000 --- /dev/null +++ b/examples/screenshot.rs @@ -0,0 +1,58 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +use std::{fs::File, io::Write}; + +use tao::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use wry::WebViewBuilder; + +fn main() -> wry::Result<()> { + // Build our event loop + let event_loop = EventLoop::new(); + let event_proxy = event_loop.create_proxy(); + // Build the window + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop) + .unwrap(); + // Build the webview + let webview = WebViewBuilder::new_as_child(&window) + .with_bounds(wry::Rect { + position: dpi::Position::Logical(dpi::LogicalPosition { x: 50.0, y: 50.0 }), + size: dpi::Size::Logical(dpi::LogicalSize { + width: 600.0, + height: 400.0, + }), + }) + .with_url("https://html5test.com") + .with_on_page_load_handler(move |_, _| event_proxy.send_event(()).unwrap()) + .build()?; + + // launch WRY process + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::UserEvent(()) => { + let on_screenshot = |image: wry::Result>| { + let image = image.expect("No image?"); + let mut file = File::create("baaaaar.png").expect("Couldn't create the dang file"); + file + .write(image.as_slice()) + .expect("Couldn't write the dang file"); + }; + webview.screenshot(on_screenshot).expect("Take screenshot") + } + _ => (), + } + }); +} diff --git a/examples/streaming.rs b/examples/streaming.rs index f916f9658..858fbc38e 100644 --- a/examples/streaming.rs +++ b/examples/streaming.rs @@ -122,7 +122,7 @@ fn stream_protocol( .decode_utf8_lossy() .to_string(); - let mut file = std::fs::File::open(&path)?; + let mut file = std::fs::File::open(path)?; // get file length let len = { diff --git a/src/android/mod.rs b/src/android/mod.rs index 37d186ce0..94a5ddcd1 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -371,6 +371,14 @@ impl InnerWebView { // Unsupported Ok(()) } + + pub fn screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) -> () + 'static + Send, + { + // Unsupported + Ok(()) + } } #[derive(Clone, Copy)] diff --git a/src/error.rs b/src/error.rs index afe0547e7..ca472d0e4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,4 +57,15 @@ pub enum Error { #[cfg(target_os = "android")] #[error(transparent)] CrossBeamRecvError(#[from] crossbeam_channel::RecvError), + + #[cfg(target_os = "linux")] + #[error(transparent)] + CairoError(#[from] cairo::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + CairoIoError(#[from] cairo::IoError), + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[error("Could not obtain screenshot from webview")] + NilScreenshot() } diff --git a/src/lib.rs b/src/lib.rs index 49634791c..1c77f0fea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1443,6 +1443,13 @@ impl WebView { pub fn focus(&self) -> Result<()> { self.webview.focus() } + + pub fn screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) + 'static + Send, + { + self.webview.screenshot(handler) + } } /// An event describing drag and drop operations on the webview. diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index 038d214a5..679020b18 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use cairo::ImageSurface; use dpi::{LogicalPosition, LogicalSize}; use gdkx11::{ ffi::{gdk_x11_window_foreign_new_for_display, GdkX11Display}, @@ -19,6 +20,7 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; #[cfg(any(debug_assertions, feature = "devtools"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ + convert::TryFrom, ffi::c_ulong, sync::{Arc, Mutex}, }; @@ -27,8 +29,8 @@ use webkit2gtk::WebInspectorExt; use webkit2gtk::{ AutoplayPolicy, InputMethodContextExt, LoadEvent, NavigationPolicyDecision, NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType, - PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames, - UserContentManagerExt, UserScript, UserScriptInjectionTime, + PrintOperationExt, SettingsExt, SnapshotOptions, SnapshotRegion, URIRequest, URIRequestExt, + UserContentInjectedFrames, UserContentManagerExt, UserScript, UserScriptInjectionTime, WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt, WebsiteDataManagerExt, WebsiteDataManagerExtManual, WebsitePolicies, }; @@ -808,6 +810,37 @@ impl InnerWebView { Ok(()) } + + pub fn screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) -> () + 'static + Send, + { + let cancellable: Option<&Cancellable> = None; + let cb = move |result: std::result::Result| match result { + Ok(surface) => match ImageSurface::try_from(surface) { + Ok(image) => { + let mut bytes = Vec::new(); + match image.write_to_png(&mut bytes) { + Ok(_) => handler(Ok(bytes)), + Err(err) => handler(Err(Error::CairoIoError(err))), + } + } + Err(_) => handler(Err(Error::CairoError( + cairo::Error::SurfaceTypeMismatch, + ))), + }, + Err(err) => handler(Err(Error::GlibError(err))), + }; + + self.webview.snapshot( + SnapshotRegion::Visible, + SnapshotOptions::NONE, + cancellable, + cb, + ); + + Ok(()) + } } pub fn platform_webview_version() -> Result { diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index fffe36401..dfcd6f2f0 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -21,7 +21,7 @@ use windows::{ Globalization::{self, MAX_LOCALE_NAME}, Graphics::Gdi::{MapWindowPoints, RedrawWindow, HBRUSH, HRGN, RDW_INTERNALPAINT}, System::{ - Com::{CoInitializeEx, IStream, COINIT_APARTMENTTHREADED}, + Com::{CoInitializeEx, IStream, COINIT_APARTMENTTHREADED, STREAM_SEEK_SET}, LibraryLoader::GetModuleHandleW, WinRT::EventRegistrationToken, }, @@ -1373,6 +1373,49 @@ impl InnerWebView { pub fn is_devtools_open(&self) -> bool { false } + + pub fn screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) + 'static + Send, + { + unsafe { + let stream = SHCreateMemStream(None); + let _ = self.webview.CapturePreview( + COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG, + stream.clone().as_ref(), + &CapturePreviewCompletedHandler::create(Box::new(move |res| { + res?; + + let mut bytes = Vec::new(); + if let Some(stream) = stream { + stream.Seek(0, STREAM_SEEK_SET, None)?; + let mut buffer: [u8; 1024] = [0; 1024]; + loop { + let mut cb_read = 0; + + stream + .Read( + buffer.as_mut_ptr() as *mut _, + buffer.len() as u32, + Some(&mut cb_read), + ) + .ok()?; + + if cb_read == 0 { + break; + } + + bytes.extend_from_slice(&buffer[..(cb_read as usize)]); + } + } + handler(Ok(bytes)); + + Ok(()) + })), + ); + } + Ok(()) + } } #[inline] diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 31a434728..f281ffce2 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -32,7 +32,10 @@ use std::{ sync::{Arc, Mutex}, }; -use core_graphics::geometry::{CGPoint, CGRect, CGSize}; +use core_graphics::{ + display::CGRectNull, + geometry::{CGPoint, CGRect, CGSize}, +}; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel, BOOL}, @@ -1256,6 +1259,75 @@ r#"Object.defineProperty(window, 'ipc', { Ok(()) } + + pub fn screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) -> () + 'static + Send, + { + unsafe { + let bounds: CGRect = msg_send![self.webview, bounds]; + let eps: id = msg_send![self.webview, dataWithEPSInsideRect:bounds]; + let ns_image: id = msg_send![class!(NSImage), alloc]; + let () = msg_send![ns_image, initWithData:eps]; + + // let context: id = msg_send![class!(NSGraphicsContext), currentContext]; + // let dict: id = msg_send![class!(NSDictionary), alloc]; + // let () = msg_send![dict, init]; + let image: id = msg_send![ns_image, CGImageForProposedRect:nil context:nil hints:nil]; + + let bitmap_image_ref: id = msg_send![class!(NSBitmapImageRep), alloc]; + let newrep: id = msg_send![bitmap_image_ref, initWithCGImage: image]; + let nsdata: id = msg_send![newrep, representationUsingType:4 properties:nil]; + let bytes: *const u8 = msg_send![nsdata, bytes]; + let len: usize = msg_send![nsdata, length]; + let vector = slice::from_raw_parts(bytes, len).to_vec(); + handler(Ok(vector)); + } + Ok(()) + } + + pub fn deprecated_screenshot(&self, handler: F) -> Result<()> + where + F: Fn(Result>) -> () + 'static + Send, + { + unsafe { + let ns_window: id = msg_send![self.webview, window]; + let window_frame: CGRect = msg_send![ns_window, frame]; + let content_view: id = msg_send![ns_window, contentView]; + + let screens: id = msg_send![class!(NSScreen), screens]; + let primary_screen: id = msg_send![screens, objectAtIndex:0]; + let primary_frame: CGRect = msg_send![primary_screen, frame]; + + let bounds: CGRect = msg_send![self.webview, bounds]; + let mut origin_rect: CGRect = msg_send![self.webview, convertRect:bounds toView:content_view]; + origin_rect.origin.x += window_frame.origin.x; + origin_rect.origin.y = primary_frame.size.height + - window_frame.origin.y + - origin_rect.origin.y + - origin_rect.size.height; + + let window_id: u32 = msg_send![ns_window, windowNumber]; + let image = core_graphics::window::create_image( + origin_rect, + core_graphics::window::kCGWindowListOptionIncludingWindow + & core_graphics::window::kCGWindowListOptionOnScreenAboveWindow + & core_graphics::window::kCGWindowListExcludeDesktopElements, + window_id, + core_graphics::window::kCGWindowImageNominalResolution, + ) + .unwrap(); + + let bitmap_image_ref: id = msg_send![class!(NSBitmapImageRep), alloc]; + let newrep: id = msg_send![bitmap_image_ref, initWithCGImage: image]; + let nsdata: id = msg_send![newrep, representationUsingType:4 properties:nil]; + let bytes: *const u8 = msg_send![nsdata, bytes]; + let len: usize = msg_send![nsdata, length]; + let vector = slice::from_raw_parts(bytes, len).to_vec(); + handler(Ok(vector)); + } + Ok(()) + } } pub fn url_from_webview(webview: id) -> Result {