Skip to content

Commit cb286e5

Browse files
authored
Screenshots in wasm (#8455)
# Objective - Enable taking a screenshot in wasm - Followup on #7163 ## Solution - Create a blob from the image data, generate a url to that blob, add an `a` element to the document linking to that url, click on that element, then revoke the url - This will automatically trigger a download of the screenshot file in the browser
1 parent 670f3f0 commit cb286e5

File tree

4 files changed

+71
-4
lines changed

4 files changed

+71
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1968,7 +1968,7 @@ path = "examples/window/screenshot.rs"
19681968
name = "Screenshot"
19691969
description = "Shows how to save screenshots to disk"
19701970
category = "Window"
1971-
wasm = false
1971+
wasm = true
19721972

19731973
[[example]]
19741974
name = "transparent_window"

crates/bevy_render/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,16 @@ encase = { version = "0.5", features = ["glam"] }
8282
# For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans.
8383
profiling = { version = "1", features = ["profile-with-tracing"], optional = true }
8484
async-channel = "1.8"
85+
86+
[target.'cfg(target_arch = "wasm32")'.dependencies]
87+
js-sys = "0.3"
88+
web-sys = { version = "0.3", features = [
89+
'Blob',
90+
'Document',
91+
'Element',
92+
'HtmlElement',
93+
'Node',
94+
'Url',
95+
'Window',
96+
] }
97+
wasm-bindgen = "0.2"

crates/bevy_render/src/view/window/screenshot.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,47 @@ impl ScreenshotManager {
7171
// discard the alpha channel which stores brightness values when HDR is enabled to make sure
7272
// the screenshot looks right
7373
let img = dyn_img.to_rgb8();
74+
#[cfg(not(target_arch = "wasm32"))]
7475
match img.save_with_format(&path, format) {
7576
Ok(_) => info!("Screenshot saved to {}", path.display()),
7677
Err(e) => error!("Cannot save screenshot, IO error: {e}"),
7778
}
79+
80+
#[cfg(target_arch = "wasm32")]
81+
{
82+
match (|| {
83+
use image::EncodableLayout;
84+
use wasm_bindgen::{JsCast, JsValue};
85+
86+
let mut image_buffer = std::io::Cursor::new(Vec::new());
87+
img.write_to(&mut image_buffer, format)
88+
.map_err(|e| JsValue::from_str(&format!("{e}")))?;
89+
// SAFETY: `image_buffer` only exist in this closure, and is not used after this line
90+
let parts = js_sys::Array::of1(&unsafe {
91+
js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes())
92+
.into()
93+
});
94+
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?;
95+
let url = web_sys::Url::create_object_url_with_blob(&blob)?;
96+
let window = web_sys::window().unwrap();
97+
let document = window.document().unwrap();
98+
let link = document.create_element("a")?;
99+
link.set_attribute("href", &url)?;
100+
link.set_attribute(
101+
"download",
102+
path.file_name()
103+
.and_then(|filename| filename.to_str())
104+
.ok_or_else(|| JsValue::from_str("Invalid filename"))?,
105+
)?;
106+
let html_element = link.dyn_into::<web_sys::HtmlElement>()?;
107+
html_element.click();
108+
web_sys::Url::revoke_object_url(&url)?;
109+
Ok::<(), JsValue>(())
110+
})() {
111+
Ok(_) => info!("Screenshot saved to {}", path.display()),
112+
Err(e) => error!("Cannot save screenshot, error: {e:?}"),
113+
};
114+
}
78115
}
79116
Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),
80117
},

examples/window/screenshot.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ fn main() {
88
App::new()
99
.add_plugins(DefaultPlugins)
1010
.add_systems(Startup, setup)
11-
.add_systems(Update, screenshot_on_f12)
11+
.add_systems(Update, screenshot_on_spacebar)
1212
.run();
1313
}
1414

15-
fn screenshot_on_f12(
15+
fn screenshot_on_spacebar(
1616
input: Res<Input<KeyCode>>,
1717
main_window: Query<Entity, With<PrimaryWindow>>,
1818
mut screenshot_manager: ResMut<ScreenshotManager>,
1919
mut counter: Local<u32>,
2020
) {
21-
if input.just_pressed(KeyCode::F12) {
21+
if input.just_pressed(KeyCode::Space) {
2222
let path = format!("./screenshot-{}.png", *counter);
2323
*counter += 1;
2424
screenshot_manager
@@ -61,4 +61,21 @@ fn setup(
6161
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
6262
..default()
6363
});
64+
65+
commands.spawn(
66+
TextBundle::from_section(
67+
"Press <spacebar> to save a screenshot to disk",
68+
TextStyle {
69+
font_size: 25.0,
70+
color: Color::WHITE,
71+
..default()
72+
},
73+
)
74+
.with_style(Style {
75+
position_type: PositionType::Absolute,
76+
top: Val::Px(10.0),
77+
left: Val::Px(10.0),
78+
..default()
79+
}),
80+
);
6481
}

0 commit comments

Comments
 (0)