From cb286e5b60ebe8255daaaa5076a3490aa03573e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 28 Apr 2023 21:37:11 +0200 Subject: [PATCH] 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 --- Cargo.toml | 2 +- crates/bevy_render/Cargo.toml | 13 +++++++ .../bevy_render/src/view/window/screenshot.rs | 37 +++++++++++++++++++ examples/window/screenshot.rs | 23 ++++++++++-- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e25ee76b4d89..0f6e97f8fe1d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1968,7 +1968,7 @@ path = "examples/window/screenshot.rs" name = "Screenshot" description = "Shows how to save screenshots to disk" category = "Window" -wasm = false +wasm = true [[example]] name = "transparent_window" diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e0f307650f5fe..7cebc4f0218f6 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -82,3 +82,16 @@ encase = { version = "0.5", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = ["profile-with-tracing"], optional = true } async-channel = "1.8" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" +web-sys = { version = "0.3", features = [ + 'Blob', + 'Document', + 'Element', + 'HtmlElement', + 'Node', + 'Url', + 'Window', +] } +wasm-bindgen = "0.2" diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 3ddca122568ae..e201424686330 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -71,10 +71,47 @@ impl ScreenshotManager { // discard the alpha channel which stores brightness values when HDR is enabled to make sure // the screenshot looks right let img = dyn_img.to_rgb8(); + #[cfg(not(target_arch = "wasm32"))] match img.save_with_format(&path, format) { Ok(_) => info!("Screenshot saved to {}", path.display()), Err(e) => error!("Cannot save screenshot, IO error: {e}"), } + + #[cfg(target_arch = "wasm32")] + { + match (|| { + use image::EncodableLayout; + use wasm_bindgen::{JsCast, JsValue}; + + let mut image_buffer = std::io::Cursor::new(Vec::new()); + img.write_to(&mut image_buffer, format) + .map_err(|e| JsValue::from_str(&format!("{e}")))?; + // SAFETY: `image_buffer` only exist in this closure, and is not used after this line + let parts = js_sys::Array::of1(&unsafe { + js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes()) + .into() + }); + let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?; + let url = web_sys::Url::create_object_url_with_blob(&blob)?; + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let link = document.create_element("a")?; + link.set_attribute("href", &url)?; + link.set_attribute( + "download", + path.file_name() + .and_then(|filename| filename.to_str()) + .ok_or_else(|| JsValue::from_str("Invalid filename"))?, + )?; + let html_element = link.dyn_into::()?; + html_element.click(); + web_sys::Url::revoke_object_url(&url)?; + Ok::<(), JsValue>(()) + })() { + Ok(_) => info!("Screenshot saved to {}", path.display()), + Err(e) => error!("Cannot save screenshot, error: {e:?}"), + }; + } } Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"), }, diff --git a/examples/window/screenshot.rs b/examples/window/screenshot.rs index 951a46fd79a4d..4b9eda146d813 100644 --- a/examples/window/screenshot.rs +++ b/examples/window/screenshot.rs @@ -8,17 +8,17 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, screenshot_on_f12) + .add_systems(Update, screenshot_on_spacebar) .run(); } -fn screenshot_on_f12( +fn screenshot_on_spacebar( input: Res>, main_window: Query>, mut screenshot_manager: ResMut, mut counter: Local, ) { - if input.just_pressed(KeyCode::F12) { + if input.just_pressed(KeyCode::Space) { let path = format!("./screenshot-{}.png", *counter); *counter += 1; screenshot_manager @@ -61,4 +61,21 @@ fn setup( transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }); + + commands.spawn( + TextBundle::from_section( + "Press to save a screenshot to disk", + TextStyle { + font_size: 25.0, + color: Color::WHITE, + ..default() + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); }