From 10b5b77b22ebeb38a5fc16249cbdb1ae6f3e98dd Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 17:21:53 +0800 Subject: [PATCH 01/18] Read `default_window_icon` from app resources --- crates/tauri-codegen/src/context.rs | 20 +---- crates/tauri/src/image/mod.rs | 113 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 415b7413fcf3..918dc72c06c9 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!(::std::option::Option::Some(#root::image::Image::from_app_icon())) } else { // handle default window icons for Unix targets let icon_path = find_icon( diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 9dec4165802b..d0ecbb095b83 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -9,6 +9,22 @@ pub(crate) mod plugin; use std::borrow::Cow; use std::sync::Arc; +#[cfg(windows)] +use windows::{ + core::PCWSTR, + Win32::{ + Graphics::Gdi::{ + CreateCompatibleDC, CreateDIBSection, DeleteDC, GetDC, ReleaseDC, SelectObject, BITMAPINFO, + BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + }, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + DrawIconEx, LoadImageW, DI_IMAGE, HICON, IDI_APPLICATION, IMAGE_ICON, LR_DEFAULTSIZE, + LR_SHARED, + }, + }, +}; + use crate::{Resource, ResourceId, ResourceTable}; /// An RGBA Image in row-major order from top to bottom. @@ -98,6 +114,103 @@ impl<'a> Image<'a> { Self::from_bytes(&bytes) } + /// test + #[cfg(windows)] + pub fn from_app_icon() -> Self { + Image::from_resource(IDI_APPLICATION, 64, 64) + } + + /// test + #[cfg(windows)] + pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> Self { + let width = width as i32; + let height = height as i32; + let color_depth_bytes = 4; + let handle = unsafe { + LoadImageW( + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + resource_id, + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE | LR_SHARED, + ) + .unwrap() + }; + + let hdc = unsafe { CreateCompatibleDC(None) }; + + let mut bitmap_info = BITMAPINFO::default(); + bitmap_info.bmiHeader.biSize = std::mem::size_of::() as _; + bitmap_info.bmiHeader.biWidth = width; + // nagative value for top-down + bitmap_info.bmiHeader.biHeight = -height; + bitmap_info.bmiHeader.biPlanes = 1; + bitmap_info.bmiHeader.biBitCount = color_depth_bytes * 8; + bitmap_info.bmiHeader.biCompression = BI_RGB.0; + + let h_dc_bitmap = unsafe { GetDC(None) }; + + let mut pv_image_bits = std::ptr::null_mut(); + + let hbitmap = unsafe { + CreateDIBSection( + Some(h_dc_bitmap), + &bitmap_info, + DIB_RGB_COLORS, + &mut pv_image_bits, + None, + 0, + ) + .unwrap() + }; + + unsafe { ReleaseDC(None, h_dc_bitmap) }; + + let h_bitmap_old = unsafe { SelectObject(hdc, hbitmap.into()) }; + + unsafe { + DrawIconEx( + hdc, + 0, + 0, + HICON(handle.0), + width, + height, + 0, + None, + // We use `DI_NORMAL` instead of `DI_NORMAL` here so it doesn't apply premultiplied alpha values + DI_IMAGE, + ) + .unwrap() + }; + + let mut bgra = unsafe { + std::slice::from_raw_parts( + pv_image_bits as *mut u8, + (width * height * color_depth_bytes as i32) as usize, + ) + .to_owned() + }; + + let rgba = { + for px in bgra.chunks_exact_mut(color_depth_bytes as usize) { + // Swap Blue and Red channels + px.swap(0, 2); + } + bgra + }; + + let image = Image::new_owned(rgba, width as u32, height as u32); + + unsafe { + SelectObject(hdc, h_bitmap_old); + DeleteDC(hdc).unwrap(); + } + + image + } + /// Returns the RGBA data for this image, in row-major order from top to bottom. pub fn rgba(&'a self) -> &'a [u8] { &self.rgba From 03975caa8b8a05aaac9b5881df42f284ed1ab6aa Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 17:33:48 +0800 Subject: [PATCH 02/18] Add windows features --- crates/tauri/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 874b986aaac1..5e555177de59 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -137,6 +137,8 @@ windows = { version = "0.61", features = [ "Win32_Foundation", "Win32_UI", "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", ] } # mobile From 95cde5aab2d5388b35c62067d8d5bd109bc4df70 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 17:46:28 +0800 Subject: [PATCH 03/18] Add docs --- crates/tauri/src/image/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index d0ecbb095b83..df9deac349ef 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -114,13 +114,13 @@ impl<'a> Image<'a> { Self::from_bytes(&bytes) } - /// test + /// Creates a new image from the application icon embedded in this executable or library. #[cfg(windows)] pub fn from_app_icon() -> Self { Image::from_resource(IDI_APPLICATION, 64, 64) } - /// test + /// Create a new image from a resource embedded in this executable or library. #[cfg(windows)] pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> Self { let width = width as i32; From c3ea82fd5a0b216d91e2dbf77c125c8337626c09 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 17:52:47 +0800 Subject: [PATCH 04/18] Add error handling --- crates/tauri-codegen/src/context.rs | 2 +- crates/tauri/src/error.rs | 4 ++++ crates/tauri/src/image/mod.rs | 13 +++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 918dc72c06c9..0cb51847cd95 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -211,7 +211,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let default_window_icon = { if target == Target::Windows { // handle default window icons for Windows targets - quote!(::std::option::Option::Some(#root::image::Image::from_app_icon())) + quote!(#root::image::Image::from_app_icon().ok()) } else { // handle default window icons for Unix targets let icon_path = find_icon( diff --git a/crates/tauri/src/error.rs b/crates/tauri/src/error.rs index 405a9b48dd1e..40ba2d421ec5 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_resource`] failed + #[error("Can not load Image from 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 df9deac349ef..580a174fced3 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -116,13 +116,14 @@ impl<'a> Image<'a> { /// Creates a new image from the application icon embedded in this executable or library. #[cfg(windows)] - pub fn from_app_icon() -> Self { + pub fn from_app_icon() -> crate::Result { Image::from_resource(IDI_APPLICATION, 64, 64) } + // TODO: Release memory even if we failed half ways /// Create a new image from a resource embedded in this executable or library. #[cfg(windows)] - pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> Self { + pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> crate::Result { let width = width as i32; let height = height as i32; let color_depth_bytes = 4; @@ -135,7 +136,7 @@ impl<'a> Image<'a> { height, LR_DEFAULTSIZE | LR_SHARED, ) - .unwrap() + .map_err(crate::Error::ImageFromResource)? }; let hdc = unsafe { CreateCompatibleDC(None) }; @@ -162,7 +163,7 @@ impl<'a> Image<'a> { None, 0, ) - .unwrap() + .map_err(crate::Error::ImageFromResource)? }; unsafe { ReleaseDC(None, h_dc_bitmap) }; @@ -182,7 +183,7 @@ impl<'a> Image<'a> { // We use `DI_NORMAL` instead of `DI_NORMAL` here so it doesn't apply premultiplied alpha values DI_IMAGE, ) - .unwrap() + .map_err(crate::Error::ImageFromResource)? }; let mut bgra = unsafe { @@ -208,7 +209,7 @@ impl<'a> Image<'a> { DeleteDC(hdc).unwrap(); } - image + Ok(image) } /// Returns the RGBA data for this image, in row-major order from top to bottom. From eb0ca561a5d726a74b79e8b59d5e0d657f0288e8 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 18:36:26 +0800 Subject: [PATCH 05/18] Adress free failed half ways todo --- crates/tauri/src/image/mod.rs | 84 +++++++++++++++-------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 580a174fced3..8315c559363c 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -11,11 +11,11 @@ use std::sync::Arc; #[cfg(windows)] use windows::{ - core::PCWSTR, + core::{Owned, PCWSTR}, Win32::{ Graphics::Gdi::{ CreateCompatibleDC, CreateDIBSection, DeleteDC, GetDC, ReleaseDC, SelectObject, BITMAPINFO, - BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HGDIOBJ, }, System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::{ @@ -120,80 +120,73 @@ impl<'a> Image<'a> { Image::from_resource(IDI_APPLICATION, 64, 64) } - // TODO: Release memory even if we failed half ways /// Create a new image from a resource embedded in this executable or library. #[cfg(windows)] pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> crate::Result { - let width = width as i32; - let height = height as i32; + let width_i32 = width as i32; + let height_i32 = height as i32; let color_depth_bytes = 4; - let handle = unsafe { - LoadImageW( - GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), - resource_id, - IMAGE_ICON, - width, - height, - LR_DEFAULTSIZE | LR_SHARED, - ) - .map_err(crate::Error::ImageFromResource)? - }; - let hdc = unsafe { CreateCompatibleDC(None) }; + let hicon = unsafe { + Owned::new(HICON( + LoadImageW( + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + resource_id, + IMAGE_ICON, + width_i32, + height_i32, + LR_DEFAULTSIZE | LR_SHARED, + ) + .map_err(crate::Error::ImageFromResource)? + .0, + )) + }; let mut bitmap_info = BITMAPINFO::default(); bitmap_info.bmiHeader.biSize = std::mem::size_of::() as _; - bitmap_info.bmiHeader.biWidth = width; + bitmap_info.bmiHeader.biWidth = width_i32; // nagative value for top-down - bitmap_info.bmiHeader.biHeight = -height; + bitmap_info.bmiHeader.biHeight = -height_i32; bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biBitCount = color_depth_bytes * 8; bitmap_info.bmiHeader.biCompression = BI_RGB.0; - let h_dc_bitmap = unsafe { GetDC(None) }; - let mut pv_image_bits = std::ptr::null_mut(); - let hbitmap = unsafe { - CreateDIBSection( + let hbitmap: Owned = unsafe { + let h_dc_bitmap = GetDC(None); + let hbitmap = CreateDIBSection( Some(h_dc_bitmap), &bitmap_info, DIB_RGB_COLORS, &mut pv_image_bits, None, 0, - ) - .map_err(crate::Error::ImageFromResource)? + ); + ReleaseDC(None, h_dc_bitmap); + Owned::new(hbitmap.map_err(crate::Error::ImageFromResource)?.into()) }; - unsafe { ReleaseDC(None, h_dc_bitmap) }; - - let h_bitmap_old = unsafe { SelectObject(hdc, hbitmap.into()) }; + let hdc = unsafe { CreateCompatibleDC(None) }; + let _h_bitmap_old = unsafe { Owned::new(SelectObject(hdc, *hbitmap)) }; unsafe { - DrawIconEx( - hdc, - 0, - 0, - HICON(handle.0), - width, - height, - 0, - None, + let result = DrawIconEx( + hdc, 0, 0, *hicon, width_i32, height_i32, 0, None, // We use `DI_NORMAL` instead of `DI_NORMAL` here so it doesn't apply premultiplied alpha values DI_IMAGE, - ) - .map_err(crate::Error::ImageFromResource)? + ); + let _ = DeleteDC(hdc); + result.map_err(crate::Error::ImageFromResource)?; }; let mut bgra = unsafe { std::slice::from_raw_parts( pv_image_bits as *mut u8, - (width * height * color_depth_bytes as i32) as usize, + (width_i32 * height_i32 * color_depth_bytes as i32) as usize, ) .to_owned() }; - let rgba = { for px in bgra.chunks_exact_mut(color_depth_bytes as usize) { // Swap Blue and Red channels @@ -202,14 +195,7 @@ impl<'a> Image<'a> { bgra }; - let image = Image::new_owned(rgba, width as u32, height as u32); - - unsafe { - SelectObject(hdc, h_bitmap_old); - DeleteDC(hdc).unwrap(); - } - - Ok(image) + Ok(Image::new_owned(rgba, width, height)) } /// Returns the RGBA data for this image, in row-major order from top to bottom. From 344f8e05ca334b3114d230f750218660e3447a8c Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 18:56:05 +0800 Subject: [PATCH 06/18] Add size option for `from_app_icon` --- crates/tauri-codegen/src/context.rs | 2 +- crates/tauri/src/image/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 0cb51847cd95..017e31ab4144 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -211,7 +211,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let default_window_icon = { if target == Target::Windows { // handle default window icons for Windows targets - quote!(#root::image::Image::from_app_icon().ok()) + quote!(#root::image::Image::from_app_icon(64).ok()) } else { // handle default window icons for Unix targets let icon_path = find_icon( diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 8315c559363c..460a61db833e 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -116,8 +116,8 @@ impl<'a> Image<'a> { /// Creates a new image from the application icon embedded in this executable or library. #[cfg(windows)] - pub fn from_app_icon() -> crate::Result { - Image::from_resource(IDI_APPLICATION, 64, 64) + pub fn from_app_icon(size: u32) -> crate::Result { + Image::from_resource(IDI_APPLICATION, size, size) } /// Create a new image from a resource embedded in this executable or library. From 7be100a6829658fe32b788e68e9fea445158af0e Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 23:08:31 +0800 Subject: [PATCH 07/18] Handle GetModuleHandleW error --- crates/tauri/src/image/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 460a61db833e..bf8dad469eb2 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -130,7 +130,11 @@ impl<'a> Image<'a> { let hicon = unsafe { Owned::new(HICON( LoadImageW( - GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + Some( + GetModuleHandleW(PCWSTR::null()) + .map_err(crate::Error::ImageFromResource)? + .into(), + ), resource_id, IMAGE_ICON, width_i32, From e08e22a21c50b1b5371a249688095d58222c8645 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 23:09:01 +0800 Subject: [PATCH 08/18] Use 32512 instead of `IDI_APPLICATION` --- crates/tauri/src/image/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index bf8dad469eb2..1a894c82b777 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -19,8 +19,7 @@ use windows::{ }, System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::{ - DrawIconEx, LoadImageW, DI_IMAGE, HICON, IDI_APPLICATION, IMAGE_ICON, LR_DEFAULTSIZE, - LR_SHARED, + DrawIconEx, LoadImageW, DI_IMAGE, HICON, IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, }, }, }; @@ -117,10 +116,19 @@ impl<'a> Image<'a> { /// Creates a new image from the application icon embedded in this executable or library. #[cfg(windows)] pub fn from_app_icon(size: u32) -> crate::Result { - Image::from_resource(IDI_APPLICATION, size, size) + Image::from_resource(PCWSTR(32512 as _), size, size) } /// Create a new image from a resource embedded in this executable or library. + /// + /// ## Examples + /// + /// The `resource_id` can be an `u16` wrapped as `PCWSTR(1 as _)` or a wide string like `w!("icon")` + /// + /// ```no_run + /// Image::from_resource(PCWSTR(1 as _), 32, 32); + /// Image::from_resource(w!("icon"), 32, 32); + /// ``` #[cfg(windows)] pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> crate::Result { let width_i32 = width as i32; From 5baff8a1cbe15db26c2022a9b4530b716449a16b Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 23:25:39 +0800 Subject: [PATCH 09/18] Add comments about `IDI_APPLICATION` --- crates/tauri-build/src/lib.rs | 6 ++++++ crates/tauri/src/image/mod.rs | 1 + 2 files changed, 7 insertions(+) diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index cdf44f50edc3..bc2446274be7 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -664,6 +664,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` + // + // `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/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 1a894c82b777..f05ec8e54095 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -116,6 +116,7 @@ impl<'a> Image<'a> { /// Creates a new image from the application icon embedded in this executable or library. #[cfg(windows)] pub fn from_app_icon(size: u32) -> crate::Result { + // Make sure we keep this `resource_id` in sync with the one in `tauri-build` Image::from_resource(PCWSTR(32512 as _), size, size) } From ae246cdc70674040937fbeb83524d8ea21bd5582 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 21 Apr 2026 23:37:11 +0800 Subject: [PATCH 10/18] Guess it still runs the doc tests --- crates/tauri/src/image/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index f05ec8e54095..06b3e6abd0cb 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -126,7 +126,9 @@ impl<'a> Image<'a> { /// /// The `resource_id` can be an `u16` wrapped as `PCWSTR(1 as _)` or a wide string like `w!("icon")` /// - /// ```no_run + /// ``` + /// # use tauri::image::Image; + /// # use windows::core::{w, PCWSTR}; /// Image::from_resource(PCWSTR(1 as _), 32, 32); /// Image::from_resource(w!("icon"), 32, 32); /// ``` From dc3cf20edc3f02a6d8bfe36bb7ea6fb13bb08e91 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 22 Apr 2026 10:32:54 +0800 Subject: [PATCH 11/18] Use `GetIconInfo` and `GetDIBits` for faster loads --- crates/tauri/src/image/mod.rs | 57 ++++++++++++++--------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 06b3e6abd0cb..db1b8dd89867 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -13,13 +13,13 @@ use std::sync::Arc; use windows::{ core::{Owned, PCWSTR}, Win32::{ + Foundation::GetLastError, Graphics::Gdi::{ - CreateCompatibleDC, CreateDIBSection, DeleteDC, GetDC, ReleaseDC, SelectObject, BITMAPINFO, - BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HGDIOBJ, + CreateCompatibleDC, DeleteDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, }, System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::{ - DrawIconEx, LoadImageW, DI_IMAGE, HICON, IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, + GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, }, }, }; @@ -162,46 +162,33 @@ impl<'a> Image<'a> { bitmap_info.bmiHeader.biWidth = width_i32; // nagative value for top-down bitmap_info.bmiHeader.biHeight = -height_i32; - bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biBitCount = color_depth_bytes * 8; + bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biCompression = BI_RGB.0; - let mut pv_image_bits = std::ptr::null_mut(); - - let hbitmap: Owned = unsafe { - let h_dc_bitmap = GetDC(None); - let hbitmap = CreateDIBSection( - Some(h_dc_bitmap), - &bitmap_info, - DIB_RGB_COLORS, - &mut pv_image_bits, - None, - 0, - ); - ReleaseDC(None, h_dc_bitmap); - Owned::new(hbitmap.map_err(crate::Error::ImageFromResource)?.into()) - }; - - let hdc = unsafe { CreateCompatibleDC(None) }; - let _h_bitmap_old = unsafe { Owned::new(SelectObject(hdc, *hbitmap)) }; + let mut icon_info = ICONINFO::default(); + unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? }; + let image_bytes = (width_i32 * height_i32 * color_depth_bytes as i32) as usize; + let mut bgra: Vec = Vec::with_capacity(image_bytes); unsafe { - let result = DrawIconEx( - hdc, 0, 0, *hicon, width_i32, height_i32, 0, None, - // We use `DI_NORMAL` instead of `DI_NORMAL` here so it doesn't apply premultiplied alpha values - DI_IMAGE, + let hdc = CreateCompatibleDC(None); + let result = GetDIBits( + hdc, + icon_info.hbmColor, + 0, + height, + Some(bgra.as_mut_ptr() as _), + &mut bitmap_info, + DIB_RGB_COLORS, ); let _ = DeleteDC(hdc); - result.map_err(crate::Error::ImageFromResource)?; - }; + if result == 0 { + return Err(crate::Error::ImageFromResource(GetLastError().into())); + } + bgra.set_len(image_bytes); + } - let mut bgra = unsafe { - std::slice::from_raw_parts( - pv_image_bits as *mut u8, - (width_i32 * height_i32 * color_depth_bytes as i32) as usize, - ) - .to_owned() - }; let rgba = { for px in bgra.chunks_exact_mut(color_depth_bytes as usize) { // Swap Blue and Red channels From efed7e36c1a9da25accac640c5425c554f9c170f Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 14:16:02 +0800 Subject: [PATCH 12/18] Add change file --- .changes/image-from-resource.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changes/image-from-resource.md diff --git a/.changes/image-from-resource.md b/.changes/image-from-resource.md new file mode 100644 index 000000000000..2ec4a6dd9007 --- /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` and `Image::from_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` From 448272e6c14f7d13135b0c62dc31c3c0d48bf118 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 14:36:00 +0800 Subject: [PATCH 13/18] Rename functions --- .changes/image-from-resource.md | 2 +- crates/tauri-codegen/src/context.rs | 2 +- crates/tauri/src/image/mod.rs | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.changes/image-from-resource.md b/.changes/image-from-resource.md index 2ec4a6dd9007..d448308c0ede 100644 --- a/.changes/image-from-resource.md +++ b/.changes/image-from-resource.md @@ -5,4 +5,4 @@ "tauri-macros": minor:feat --- -Added `Image::from_app_icon` and `Image::from_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` +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-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 017e31ab4144..335fe3a9a946 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -211,7 +211,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let default_window_icon = { if target == Target::Windows { // handle default window icons for Windows targets - quote!(#root::image::Image::from_app_icon(64).ok()) + 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/src/image/mod.rs b/crates/tauri/src/image/mod.rs index db1b8dd89867..336c6ee81cd2 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -114,13 +114,18 @@ impl<'a> Image<'a> { } /// 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(size: u32) -> crate::Result { + 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_resource(PCWSTR(32512 as _), size, size) + Image::from_icon_resource(PCWSTR(32512 as _), size, size) } - /// Create a new image from a resource embedded in this executable or library. + /// 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. /// /// ## Examples /// @@ -133,7 +138,7 @@ impl<'a> Image<'a> { /// Image::from_resource(w!("icon"), 32, 32); /// ``` #[cfg(windows)] - pub fn from_resource(resource_id: PCWSTR, width: u32, height: u32) -> crate::Result { + 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; From 6b2389501ca8eee28131879f738f1bdc5917bc28 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 14:57:01 +0800 Subject: [PATCH 14/18] Use `LR_DEFAULTCOLOR` instead of `LR_DEFAULTSIZE | LR_SHARED` For `LR_DEFAULTSIZE` we never use the default size and `LR_SHARED` requires standard size whatever that is --- crates/tauri/src/image/mod.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 336c6ee81cd2..af3238a97821 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -19,7 +19,7 @@ use windows::{ }, System::LibraryLoader::GetModuleHandleW, UI::WindowsAndMessaging::{ - GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, + GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, }, }, }; @@ -155,13 +155,19 @@ impl<'a> Image<'a> { IMAGE_ICON, width_i32, height_i32, - LR_DEFAULTSIZE | LR_SHARED, + 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 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; @@ -171,11 +177,6 @@ impl<'a> Image<'a> { bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biCompression = BI_RGB.0; - let mut icon_info = ICONINFO::default(); - unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? }; - - let image_bytes = (width_i32 * height_i32 * color_depth_bytes as i32) as usize; - let mut bgra: Vec = Vec::with_capacity(image_bytes); unsafe { let hdc = CreateCompatibleDC(None); let result = GetDIBits( From c4701068c8cb2332ae498cea6f7b833331e9423f Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 15:02:39 +0800 Subject: [PATCH 15/18] Fix error docs --- crates/tauri/src/error.rs | 4 ++-- crates/tauri/src/image/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tauri/src/error.rs b/crates/tauri/src/error.rs index 40ba2d421ec5..de43e4d20615 100644 --- a/crates/tauri/src/error.rs +++ b/crates/tauri/src/error.rs @@ -167,8 +167,8 @@ pub enum Error { #[error(transparent)] TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError), #[cfg(windows)] - /// [`crate::image::Image::from_resource`] failed - #[error("Can not load Image from resources: {0}")] + /// [`crate::image::Image::from_icon_resource`] failed + #[error("Can not load Image from icon resources: {0}")] ImageFromResource(windows::core::Error), } diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index af3238a97821..30863975f983 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -125,7 +125,7 @@ impl<'a> Image<'a> { /// 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. + /// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time. /// /// ## Examples /// From 89957d52f517c0ada15babde1f10c0ea8c0eb34e Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 15:08:04 +0800 Subject: [PATCH 16/18] Update tauri-build comments --- crates/tauri-build/src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index bc2446274be7..0f8925cac878 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 @@ -664,7 +666,7 @@ 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` + // 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)`) From 6dbd221377a3b15e163871eeb0a139220dcca959 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 23 Apr 2026 15:23:52 +0800 Subject: [PATCH 17/18] Fix docs --- crates/tauri/src/image/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 30863975f983..3c4c5dd0d9f7 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -134,8 +134,8 @@ impl<'a> Image<'a> { /// ``` /// # use tauri::image::Image; /// # use windows::core::{w, PCWSTR}; - /// Image::from_resource(PCWSTR(1 as _), 32, 32); - /// Image::from_resource(w!("icon"), 32, 32); + /// 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 { From d08e9499390f00cefbceb07a1053cb457b50e43f Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 1 May 2026 19:50:31 +0800 Subject: [PATCH 18/18] Fix `ICONINFO` from `GetIconInfo` not freed --- crates/tauri/src/image/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 3ff526b381fc..27b9b6c08bb2 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -164,6 +164,8 @@ impl<'a> Image<'a> { 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); @@ -181,7 +183,7 @@ impl<'a> Image<'a> { let hdc = CreateCompatibleDC(None); let result = GetDIBits( hdc, - icon_info.hbmColor, + *hbm_color, 0, height, Some(bgra.as_mut_ptr() as _),