pub(crate) mod plugin;
use std::borrow::Cow;
use std::sync::Arc;
#[cfg(windows)]
use windows::{
Win32::{
Foundation::{E_FAIL, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, WIN32_ERROR},
Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, DIB_RGB_COLORS, DeleteDC,
GetDIBits, HBITMAP,
},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
GetIconInfo, GetSystemMetrics, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW,
SM_CXICON, SM_CYICON,
},
},
core::{Owned, PCWSTR},
};
use crate::{Resource, ResourceId, ResourceTable};
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IconResource<'a> {
Id(u16),
Name(&'a str),
}
#[cfg(windows)]
impl From<u16> for IconResource<'_> {
fn from(id: u16) -> Self {
Self::Id(id)
}
}
#[cfg(windows)]
impl<'a> From<&'a str> for IconResource<'a> {
fn from(name: &'a str) -> Self {
Self::Name(name)
}
}
#[cfg(windows)]
#[doc(hidden)]
pub fn default_window_icon_from_app_icon_resource() -> Option<Image<'static>> {
let metric = |index| match unsafe { GetSystemMetrics(index) } {
n if n > 0 => n as u32,
_ => 32,
};
let (width, height) = (metric(SM_CXICON), metric(SM_CYICON));
match Image::from_icon_resource(
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
width,
height,
) {
Ok(icon) => Some(icon),
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("failed to load the default window icon from the application icon resource: {e}");
log::warn!("failed to load the default window icon from the application icon resource: {e}");
None
}
}
}
#[cfg(windows)]
const BYTES_PER_PIXEL: usize = 4;
#[cfg(windows)]
unsafe fn read_bgra(hbm: HBITMAP, width: i32, height: i32) -> crate::Result<Vec<u8>> {
let image_bytes = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(BYTES_PER_PIXEL))
.ok_or_else(|| resource_error(ERROR_INVALID_PARAMETER, "image size overflows usize"))?;
let mut bgra = vec![0u8; image_bytes];
let mut bitmap_info = BITMAPINFO::default();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as _;
bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = -height;
bitmap_info.bmiHeader.biBitCount = (BYTES_PER_PIXEL * 8) as u16;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biCompression = BI_RGB.0;
unsafe {
let hdc = CreateCompatibleDC(None);
let scan_lines = GetDIBits(
hdc,
hbm,
0,
height as u32,
Some(bgra.as_mut_ptr() as _),
&mut bitmap_info,
DIB_RGB_COLORS,
);
let error = (scan_lines != height).then(|| {
last_error_or(&format!(
"GetDIBits copied {scan_lines} of {height} scan lines"
))
});
let _ = DeleteDC(hdc);
if let Some(error) = error {
return Err(crate::Error::ImageFromResource(error));
}
}
Ok(bgra)
}
#[cfg(windows)]
fn resource_error(code: WIN32_ERROR, message: &str) -> crate::Error {
crate::Error::ImageFromResource(windows::core::Error::new(code.to_hresult(), message))
}
#[cfg(windows)]
fn last_error_or(message: &str) -> windows::core::Error {
let error = windows::core::Error::from_thread();
if error.code().is_ok() {
windows::core::Error::new(E_FAIL, message)
} else {
error
}
}
#[derive(Clone)]
pub struct Image<'a> {
rgba: Cow<'a, [u8]>,
width: u32,
height: u32,
}
impl std::fmt::Debug for Image<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Image")
.field(
"rgba",
&format_args!(
"Cow::{}([u8; {}])",
match &self.rgba {
Cow::Borrowed(_) => "Borrowed",
Cow::Owned(_) => "Owned",
},
self.rgba.len()
),
)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
impl Resource for Image<'static> {}
impl Image<'static> {
pub const fn new_owned(rgba: Vec<u8>, width: u32, height: u32) -> Self {
Self {
rgba: Cow::Owned(rgba),
width,
height,
}
}
}
impl<'a> Image<'a> {
pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self {
Self {
rgba: Cow::Borrowed(rgba),
width,
height,
}
}
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
let img = image::load_from_memory(bytes)?;
let (width, height) = (img.width(), img.height());
Ok(Self {
rgba: Cow::Owned(img.into_rgba8().into_raw()),
width,
height,
})
}
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> crate::Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
pub fn from_app_icon_resource(size: u32) -> crate::Result<Self> {
Image::from_icon_resource(
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
size,
size,
)
}
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
pub fn from_icon_resource<'r>(
resource: impl Into<IconResource<'r>>,
width: u32,
height: u32,
) -> crate::Result<Self> {
let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) {
(Ok(w), Ok(h)) if w > 0 && h > 0 => (w, h),
_ => {
return Err(resource_error(
ERROR_INVALID_PARAMETER,
"width and height must be between 1 and i32::MAX",
));
}
};
let name: Vec<u16>;
let resource_id = match resource.into() {
IconResource::Id(id) => PCWSTR(id as usize as *const u16),
IconResource::Name(n) => {
name = n.encode_utf16().chain(std::iter::once(0)).collect();
PCWSTR(name.as_ptr())
}
};
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) };
if hbm_color.is_invalid() {
return Err(resource_error(
ERROR_NOT_SUPPORTED,
"monochrome icons are not supported",
));
}
let mut bgra = unsafe { read_bgra(*hbm_color, width_i32, height_i32)? };
if bgra
.as_chunks::<BYTES_PER_PIXEL>()
.0
.iter()
.all(|px| px[3] == 0)
{
let mask = unsafe { read_bgra(*hbm_mask, width_i32, height_i32)? };
for (px, mask) in bgra
.as_chunks_mut::<BYTES_PER_PIXEL>()
.0
.iter_mut()
.zip(mask.as_chunks::<BYTES_PER_PIXEL>().0)
{
px[3] = if mask[0] == 0 { 0xFF } else { 0 };
}
}
let rgba = {
for px in bgra.as_chunks_mut::<BYTES_PER_PIXEL>().0 {
px.swap(0, 2);
}
bgra
};
Ok(Image::new_owned(rgba, width, height))
}
pub fn rgba(&'a self) -> &'a [u8] {
&self.rgba
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn to_owned(self) -> Image<'static> {
Image {
rgba: match self.rgba {
Cow::Owned(v) => Cow::Owned(v),
Cow::Borrowed(v) => Cow::Owned(v.to_vec()),
},
height: self.height,
width: self.width,
}
}
}
impl<'a> From<Image<'a>> for crate::runtime::Icon<'a> {
fn from(img: Image<'a>) -> Self {
Self {
rgba: img.rgba,
width: img.width,
height: img.height,
}
}
}
#[cfg(desktop)]
impl TryFrom<Image<'_>> for muda::Icon {
type Error = crate::Error;
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
muda::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
}
}
#[cfg(all(desktop, feature = "tray-icon"))]
impl TryFrom<Image<'_>> for tray_icon::Icon {
type Error = crate::Error;
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
tray_icon::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
}
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsImage {
#[non_exhaustive]
Path(std::path::PathBuf),
#[non_exhaustive]
Bytes(Vec<u8>),
#[non_exhaustive]
Resource(ResourceId),
#[non_exhaustive]
Rgba {
rgba: Vec<u8>,
width: u32,
height: u32,
},
}
impl JsImage {
pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result<Arc<Image<'_>>> {
match self {
Self::Resource(rid) => resources_table.get::<Image<'static>>(rid),
#[cfg(any(feature = "image-ico", feature = "image-png"))]
Self::Path(path) => Image::from_path(path).map(Arc::new),
#[cfg(any(feature = "image-ico", feature = "image-png"))]
Self::Bytes(bytes) => Image::from_bytes(&bytes).map(Arc::new),
Self::Rgba {
rgba,
width,
height,
} => Ok(Arc::new(Image::new_owned(rgba, width, height))),
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
_ => Err(
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"expected RGBA image data, found {}",
match self {
JsImage::Path(_) => "a file path",
JsImage::Bytes(_) => "raw bytes",
_ => unreachable!(),
}
),
)
.into(),
),
}
}
}
#[cfg(all(test, windows))]
mod tests {
use super::{IconResource, Image, default_window_icon_from_app_icon_resource};
#[test]
fn from_icon_resource_missing_resource_is_an_error() {
for resource in [
IconResource::Id(u16::MAX),
IconResource::Name("tauri-image-test-missing-icon"),
] {
let error = Image::from_icon_resource(resource, 32, 32).unwrap_err();
assert!(
matches!(error, crate::Error::ImageFromResource(_)),
"{resource:?}: {error:?}"
);
}
assert!(default_window_icon_from_app_icon_resource().is_none());
}
#[test]
fn from_icon_resource_rejects_invalid_sizes() {
for (width, height) in [(0, 32), (32, 0), (u32::MAX, 32), (32, i32::MAX as u32 + 1)] {
let error = Image::from_icon_resource(1, width, height).unwrap_err();
assert!(
matches!(error, crate::Error::ImageFromResource(_)),
"{width}x{height}: {error:?}"
);
}
}
}