Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ objc2-web-kit = { version = "0.3.0", default-features = false, features = [
"WKWebpagePreferences",
"WKNavigationResponse",
"WKUserScript",
"WKSnapshotConfiguration",
"WKHTTPCookieStore",
"WKWindowFeatures",
] }
Expand Down Expand Up @@ -190,6 +191,9 @@ objc2-app-kit = { version = "0.3.0", default-features = false, features = [
"NSSavePanel",
"NSMenu",
"NSGraphics",
"NSImage",
"NSImageRep",
"NSBitmapImageRep",
"NSScreen",
] }

Expand Down
150 changes: 150 additions & 0 deletions examples/screenshot_smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;

use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
};
use wry::{PageLoadEvent, WebViewBuilder};

#[derive(Debug, Clone, Copy)]
enum UserEvent {
Capture,
Exit,
}

fn main() -> wry::Result<()> {
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("wry screenshot smoke")
.build(&event_loop)
.unwrap();

let already_requested = Arc::new(AtomicBool::new(false));
let already_requested_ = already_requested.clone();
let proxy_for_load = proxy.clone();

let builder = WebViewBuilder::new()
.with_html(
r#"<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WRY Screenshot Smoke</title>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
font-family: sans-serif;
}
body {
display: grid;
place-items: center;
background: #1f2937;
color: white;
}
.card {
padding: 24px 28px;
border-radius: 16px;
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.18);
box-shadow: 0 20px 50px rgba(0,0,0,0.35);
backdrop-filter: blur(8px);
}
h1 { margin: 0 0 8px; font-size: 28px; }
p { margin: 0; opacity: 0.9; }
</style>
</head>
<body>
<div class="card">
<h1>Screenshot Smoke Test</h1>
<p>If you can read this in screenshot.png, capture worked.</p>
</div>
</body>
</html>"#,
)
.with_on_page_load_handler(move |event, _url| {
if matches!(event, PageLoadEvent::Finished)
&& !already_requested_.swap(true, Ordering::SeqCst)
{
let proxy = proxy_for_load.clone();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(1000));
let _ = proxy.send_event(UserEvent::Capture);
});
}
});

#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let webview = builder.build(&window)?;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let webview = {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = window.default_vbox().unwrap();
builder.build_gtk(vbox)?
};

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;

match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::UserEvent(UserEvent::Capture) => {
// screenshot is not supported on Android or iOS; the handler would
// never be called, so we exit immediately on those platforms.
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let _ = proxy.send_event(UserEvent::Exit);
return;
}

#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let proxy = proxy.clone();
webview
.screenshot(move |result| {
match result {
Ok(bytes) => {
if let Err(err) = std::fs::write("screenshot.png", bytes) {
eprintln!("failed to write screenshot.png: {err}");
} else {
println!("wrote screenshot.png");
}
}
Err(err) => eprintln!("screenshot failed: {err}"),
}
let _ = proxy.send_event(UserEvent::Exit);
})
.expect("failed to request screenshot");
}
}
Event::UserEvent(UserEvent::Exit) => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}
8 changes: 8 additions & 0 deletions src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ impl InnerWebView {
Ok(())
}

pub fn screenshot<F>(&self, _handler: F) -> Result<()>
where
F: Fn(Result<Vec<u8>>) + 'static + Send,
{
// Unsupported
Ok(())
}

pub fn id(&self) -> crate::WebViewId<'_> {
&self.id
}
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ pub enum Error {
#[cfg(gtk)]
#[error("Couldn't find X11 Display")]
X11DisplayNotFound,
#[cfg(gtk)]
#[error(transparent)]
CairoError(#[from] gtk::cairo::Error),
#[cfg(gtk)]
#[error("Failed to convert WebView snapshot to a Pixbuf")]
PixbufConversionFailed,
#[cfg(all(gtk, feature = "x11"))]
#[error(transparent)]
XlibError(#[from] x11_dl::error::OpenError),
Expand Down Expand Up @@ -74,4 +80,7 @@ pub enum Error {
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[error("data store is currently opened")]
DataStoreInUse,
#[cfg(target_os = "macos")]
#[error("Could not obtain screenshot from webview")]
NilScreenshot,
}
15 changes: 15 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2033,6 +2033,21 @@ impl WebView {
self.webview.print()
}

/// Capture a PNG screenshot of the currently visible webview contents.
///
/// The screenshot is returned asynchronously via `handler`.
///
/// ## Platform-specific
///
/// - **Linux / macOS / Windows**: Implemented (visible region only).
/// - **Android / iOS**: Not supported; `handler` will never be called.
pub fn screenshot<F>(&self, handler: F) -> Result<()>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be so awesome if we could abstract away the handler but i guess that's more something for crate consumers like tauri

where
F: Fn(Result<Vec<u8>>) + 'static + Send,
{
self.webview.screenshot(handler)
}

/// Get a list of cookies for specific url.
pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
self.webview.cookies_for_url(url)
Expand Down
44 changes: 40 additions & 4 deletions src/webkitgtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ use webkit2gtk::WebInspectorExt;
use webkit2gtk::{
AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision,
NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType,
PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames,
UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime,
WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt, WebsiteDataManagerExt,
WebsiteDataManagerExtManual, WebsitePolicies,
PrintOperationExt, SettingsExt, SnapshotOptions, SnapshotRegion, URIRequest, URIRequestExt,
UserContentInjectedFrames, UserContentManager, UserContentManagerExt, UserScript,
UserScriptInjectionTime, WebContextExt as Webkit2gtkWeContextExt, WebView, WebViewExt,
WebsiteDataManagerExt, WebsiteDataManagerExtManual, WebsitePolicies,
};
use webkit2gtk_sys::{
webkit_get_major_version, webkit_get_micro_version, webkit_get_minor_version,
Expand Down Expand Up @@ -679,6 +679,42 @@ impl InnerWebView {
Ok(())
}

pub fn screenshot<F>(&self, handler: F) -> Result<()>
where
F: Fn(Result<Vec<u8>>) + 'static + Send,
{
let cancellable: Option<&Cancellable> = None;
let cb = move |result: std::result::Result<gtk::cairo::Surface, gtk::glib::Error>| match result
{
Ok(surface) => match gtk::cairo::ImageSurface::try_from(surface) {
Ok(image) => {
let width = image.width();
let height = image.height();
match gdk::pixbuf_get_from_surface(&image, 0, 0, width, height) {
Some(pixbuf) => match pixbuf.save_to_bufferv("png", &[]) {
Ok(bytes) => handler(Ok(bytes)),
Err(err) => handler(Err(Error::GlibError(err))),
},
None => handler(Err(Error::PixbufConversionFailed)),
}
}
Err(_) => handler(Err(Error::CairoError(
gtk::cairo::Error::SurfaceTypeMismatch,
))),
},
Err(err) => handler(Err(Error::GlibError(err))),
};

self.webview.snapshot(
SnapshotRegion::Visible,
SnapshotOptions::NONE,
cancellable,
cb,
);

Ok(())
}

pub fn url(&self) -> Result<String> {
Ok(self.webview.uri().unwrap_or_default().to_string())
}
Expand Down
54 changes: 54 additions & 0 deletions src/webview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,60 @@ impl InnerWebView {
)
}

pub fn screenshot<F>(&self, handler: F) -> Result<()>
where
F: Fn(Result<Vec<u8>>) + 'static + Send,
{
unsafe {
let Some(stream) = SHCreateMemStream(None) else {
return Err(Error::from(windows::core::Error::from(E_POINTER)));
};
let stream_for_handler = stream.clone();

self.webview.CapturePreview(
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG,
&stream,
&CapturePreviewCompletedHandler::create(Box::new(move |res| {
let result = (|| -> windows::core::Result<Vec<u8>> {
res?;

let mut bytes = Vec::new();
stream_for_handler.Seek(0, STREAM_SEEK_SET, None)?;

let mut buffer = [0u8; 4096];
loop {
let mut cb_read = 0;
stream_for_handler
.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]);
}

Ok(bytes)
})();

match result {
Ok(bytes) => handler(Ok(bytes)),
Err(err) => handler(Err(err.into())),
}

Ok(())
})),
)?;
}

Ok(())
}

pub fn clear_all_browsing_data(&self) -> Result<()> {
unsafe {
self
Expand Down
Loading
Loading