Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 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,7 +191,12 @@ objc2-app-kit = { version = "0.3.0", default-features = false, features = [
"NSSavePanel",
"NSMenu",
"NSGraphics",
"NSImage",
"NSImageRep",
"NSBitmapImageRep",
"NSGraphicsContext",
"NSScreen",
"objc2-core-graphics",
] }

[target."cfg(target_os = \"android\")".dependencies]
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
16 changes: 16 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,14 @@ 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,
#[cfg(target_os = "macos")]
#[error("Screenshot failed ({domain}:{code}): {description}")]
MacOsScreenshotError {
domain: String,
code: isize,
description: String,
},
}
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
Loading
Loading