diff --git a/.changes/image-from-resource.md b/.changes/image-from-resource.md new file mode 100644 index 000000000000..d448308c0ede --- /dev/null +++ b/.changes/image-from-resource.md @@ -0,0 +1,8 @@ +--- +"tauri": minor:feat +"tauri-build": minor:feat +"tauri-codegen": minor:feat +"tauri-macros": minor:feat +--- + +Added `Image::from_app_icon_resource` and `Image::from_icon_resource` on Windows for loading images from resources embedded in the executable, and the default `default_window_icon` from `tauri::generate_context` macro is now loaded using `from_app_icon_resource` diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index 6c171a3005f9..2b921e08f0be 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -256,8 +256,8 @@ impl WindowsAttributes { /// Creates the default attribute set. pub fn new() -> Self { Self { - window_icon_path: Default::default(), app_manifest: Some(include_str!("windows-app-manifest.xml").into()), + window_icon_path: None, append_rc_content: Vec::new(), } } @@ -267,13 +267,15 @@ impl WindowsAttributes { pub fn new_without_app_manifest() -> Self { Self { app_manifest: None, - window_icon_path: Default::default(), + window_icon_path: None, append_rc_content: Vec::new(), } } - /// Sets the icon to use on the window. Currently only used on Windows. - /// It must be in `ico` format. Defaults to `icons/icon.ico`. + /// Sets the icon to use as the application icon and default window icon. + /// It must be in `ico` format. + /// + /// If not set, we will search for a `.ico` from the `bundle > icon` in your tauri config file, then `icons/icon.ico`. #[must_use] pub fn window_icon_path>(mut self, window_icon_path: P) -> Self { self @@ -666,6 +668,12 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } if window_icon_path.exists() { + // Keep this `name_id` in sync with the one in `tauri::image::Image::from_app_icon_resource` + // + // `32512` has no special meaning here, + // it was used because we misunderstood `IDI_APPLICATION` (`MAKEINTRESOURCE(32512)`) + // should be used for the application icon, which is not true. + // See https://devblogs.microsoft.com/oldnewthing/20250423-00/?p=111106 res.set_icon_with_id(&window_icon_path.display().to_string(), "32512"); } else { return Err(anyhow!(format!( diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 415b7413fcf3..335fe3a9a946 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -211,25 +211,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let default_window_icon = { if target == Target::Windows { // handle default window icons for Windows targets - let icon_path = find_icon( - &config, - &config_parent, - |i| i.ends_with(".ico"), - "icons/icon.ico", - ); - if icon_path.exists() { - let icon = CachedIcon::new(&root, &icon_path)?; - quote!(::std::option::Option::Some(#icon)) - } else { - let icon_path = find_icon( - &config, - &config_parent, - |i| i.ends_with(".png"), - "icons/icon.png", - ); - let icon = CachedIcon::new(&root, &icon_path)?; - quote!(::std::option::Option::Some(#icon)) - } + quote!(#root::image::Image::from_app_icon_resource(64).ok()) } else { // handle default window icons for Unix targets let icon_path = find_icon( diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 41c518382ee9..3da3e28d5faf 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -138,6 +138,8 @@ windows = { version = "0.61", features = [ "Win32_Foundation", "Win32_UI", "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", ] } # mobile diff --git a/crates/tauri/src/error.rs b/crates/tauri/src/error.rs index 405a9b48dd1e..de43e4d20615 100644 --- a/crates/tauri/src/error.rs +++ b/crates/tauri/src/error.rs @@ -166,6 +166,10 @@ pub enum Error { /// tokio oneshot channel failed to receive message #[error(transparent)] TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError), + #[cfg(windows)] + /// [`crate::image::Image::from_icon_resource`] failed + #[error("Can not load Image from icon resources: {0}")] + ImageFromResource(windows::core::Error), } impl From for Error { diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index c25e98de5f29..27b9b6c08bb2 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -9,6 +9,21 @@ pub(crate) mod plugin; use std::borrow::Cow; use std::sync::Arc; +#[cfg(windows)] +use windows::{ + core::{Owned, PCWSTR}, + Win32::{ + Foundation::GetLastError, + Graphics::Gdi::{ + CreateCompatibleDC, DeleteDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + }, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, + }, + }, +}; + use crate::{Resource, ResourceId, ResourceTable}; /// An RGBA Image in row-major order from top to bottom. @@ -98,6 +113,101 @@ impl<'a> Image<'a> { Self::from_bytes(&bytes) } + /// Creates a new image from the application icon embedded in this executable or library. + /// + /// The application icon is currently the icon with `nameID 32512` we embedded through `tauri-build`, + /// this could change in the future. + #[cfg(windows)] + pub fn from_app_icon_resource(size: u32) -> crate::Result { + // Make sure we keep this `resource_id` in sync with the one in `tauri-build` + Image::from_icon_resource(PCWSTR(32512 as _), size, size) + } + + /// Create a new image from an icon resource embedded in this executable or library. + /// + /// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time. + /// + /// ## Examples + /// + /// The `resource_id` can be an `u16` wrapped as `PCWSTR(1 as _)` or a wide string like `w!("icon")` + /// + /// ``` + /// # use tauri::image::Image; + /// # use windows::core::{w, PCWSTR}; + /// Image::from_icon_resource(PCWSTR(1 as _), 32, 32); + /// Image::from_icon_resource(w!("icon"), 32, 32); + /// ``` + #[cfg(windows)] + pub fn from_icon_resource(resource_id: PCWSTR, width: u32, height: u32) -> crate::Result { + let width_i32 = width as i32; + let height_i32 = height as i32; + let color_depth_bytes = 4; + + let hicon = unsafe { + Owned::new(HICON( + LoadImageW( + Some( + GetModuleHandleW(PCWSTR::null()) + .map_err(crate::Error::ImageFromResource)? + .into(), + ), + resource_id, + IMAGE_ICON, + width_i32, + height_i32, + LR_DEFAULTCOLOR, + ) + .map_err(crate::Error::ImageFromResource)? + .0, + )) + }; + + let mut icon_info = ICONINFO::default(); + unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? }; + let _hbm_mask = unsafe { Owned::new(icon_info.hbmMask) }; + let hbm_color = unsafe { Owned::new(icon_info.hbmColor) }; + + let image_bytes = (width_i32 * height_i32 * color_depth_bytes as i32) as usize; + let mut bgra: Vec = Vec::with_capacity(image_bytes); + + let mut bitmap_info = BITMAPINFO::default(); + bitmap_info.bmiHeader.biSize = std::mem::size_of::() as _; + bitmap_info.bmiHeader.biWidth = width_i32; + // nagative value for top-down + bitmap_info.bmiHeader.biHeight = -height_i32; + bitmap_info.bmiHeader.biBitCount = color_depth_bytes * 8; + bitmap_info.bmiHeader.biPlanes = 1; + bitmap_info.bmiHeader.biCompression = BI_RGB.0; + + unsafe { + let hdc = CreateCompatibleDC(None); + let result = GetDIBits( + hdc, + *hbm_color, + 0, + height, + Some(bgra.as_mut_ptr() as _), + &mut bitmap_info, + DIB_RGB_COLORS, + ); + let _ = DeleteDC(hdc); + if result == 0 { + return Err(crate::Error::ImageFromResource(GetLastError().into())); + } + bgra.set_len(image_bytes); + } + + let rgba = { + for px in bgra.chunks_exact_mut(color_depth_bytes as usize) { + // Swap Blue and Red channels + px.swap(0, 2); + } + bgra + }; + + Ok(Image::new_owned(rgba, width, height)) + } + /// Returns the RGBA data for this image, in row-major order from top to bottom. pub fn rgba(&'a self) -> &'a [u8] { &self.rgba