Sfoglia il codice sorgente

feat(api): add `Image` class (#9042)

* feat(api): add `Image` class

* clippy

* license headers

* small cleanup

* fixes

* code review

* readd from_png_bytes and from_ico_bytes

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 1 anno fa
parent
commit
77b9a508a4

+ 2 - 1
.changes/tauri-image.md

@@ -1,5 +1,6 @@
 ---
 'tauri': 'minor:feat'
+'@tauri-apps/api': 'minor:feat'
 ---
 
-Add `Image` type.
+Add a new `Image` type in Rust and JS.

+ 0 - 7
Cargo.lock

@@ -3418,12 +3418,6 @@ dependencies = [
  "loom",
 ]
 
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
 [[package]]
 name = "string_cache"
 version = "0.8.7"
@@ -3630,7 +3624,6 @@ dependencies = [
  "serde_repr",
  "serialize-to-javascript",
  "state",
- "static_assertions",
  "swift-rs",
  "tauri",
  "tauri-build",

+ 0 - 1
core/tauri/Cargo.toml

@@ -72,7 +72,6 @@ png = { version = "0.17", optional = true }
 ico = { version = "0.3.0", optional = true }
 http-range = { version = "0.1.5", optional = true }
 tracing = { version = "0.1", optional = true }
-static_assertions = "1"
 heck = "0.4"
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies]

+ 13 - 0
core/tauri/build.rs

@@ -137,6 +137,19 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
       ("app_hide", false),
     ],
   ),
+  (
+    "image",
+    &[
+      ("new", true),
+      ("from_bytes", true),
+      ("from_png_bytes", true),
+      ("from_ico_bytes", true),
+      ("from_path", true),
+      ("rgba", true),
+      ("width", true),
+      ("height", true),
+    ],
+  ),
   ("resources", &[("close", true)]),
   (
     "menu",

+ 19 - 0
core/tauri/permissions/image/autogenerated/reference.md

@@ -0,0 +1,19 @@
+| Permission | Description |
+|------|-----|
+|`allow-from-bytes`|Enables the from_bytes command without any pre-configured scope.|
+|`deny-from-bytes`|Denies the from_bytes command without any pre-configured scope.|
+|`allow-from-ico-bytes`|Enables the from_ico_bytes command without any pre-configured scope.|
+|`deny-from-ico-bytes`|Denies the from_ico_bytes command without any pre-configured scope.|
+|`allow-from-path`|Enables the from_path command without any pre-configured scope.|
+|`deny-from-path`|Denies the from_path command without any pre-configured scope.|
+|`allow-from-png-bytes`|Enables the from_png_bytes command without any pre-configured scope.|
+|`deny-from-png-bytes`|Denies the from_png_bytes command without any pre-configured scope.|
+|`allow-height`|Enables the height command without any pre-configured scope.|
+|`deny-height`|Denies the height command without any pre-configured scope.|
+|`allow-new`|Enables the new command without any pre-configured scope.|
+|`deny-new`|Denies the new command without any pre-configured scope.|
+|`allow-rgba`|Enables the rgba command without any pre-configured scope.|
+|`deny-rgba`|Denies the rgba command without any pre-configured scope.|
+|`allow-width`|Enables the width command without any pre-configured scope.|
+|`deny-width`|Denies the width command without any pre-configured scope.|
+|`default`|Default permissions for the plugin.|

File diff suppressed because it is too large
+ 0 - 0
core/tauri/scripts/bundle.global.js


+ 1 - 0
core/tauri/src/app.rs

@@ -870,6 +870,7 @@ impl<R: Runtime> App<R> {
     self.handle.plugin(crate::webview::plugin::init())?;
     self.handle.plugin(crate::app::plugin::init())?;
     self.handle.plugin(crate::resources::plugin::init())?;
+    self.handle.plugin(crate::image::plugin::init())?;
     #[cfg(desktop)]
     self.handle.plugin(crate::menu::plugin::init())?;
     #[cfg(all(desktop, feature = "tray-icon"))]

+ 49 - 15
core/tauri/src/image.rs → core/tauri/src/image/mod.rs

@@ -2,8 +2,13 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+pub mod plugin;
+
 use std::borrow::Cow;
 use std::io::{Error, ErrorKind};
+use std::sync::Arc;
+
+use crate::{Manager, Resource, ResourceId, Runtime};
 
 /// An RGBA Image in row-major order from top to bottom.
 #[derive(Debug, Clone)]
@@ -13,6 +18,21 @@ pub struct Image<'a> {
   height: u32,
 }
 
+impl Resource for Image<'static> {}
+
+impl Image<'static> {
+  /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
+  ///
+  /// Similar to [`Self::new`] but avoids cloning the rgba data to get an owned Image.
+  pub const fn new_owned(rgba: Vec<u8>, width: u32, height: u32) -> Self {
+    Self {
+      rgba: Cow::Owned(rgba),
+      width,
+      height,
+    }
+  }
+}
+
 impl<'a> Image<'a> {
   /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
   pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self {
@@ -123,6 +143,19 @@ impl<'a> Image<'a> {
   pub fn height(&self) -> u32 {
     self.height
   }
+
+  /// Convert into a 'static owned [`Image`].
+  /// This will allocate.
+  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> {
@@ -153,12 +186,12 @@ impl TryFrom<Image<'_>> for tray_icon::Icon {
   }
 }
 
-#[cfg(desktop)]
 #[derive(serde::Deserialize)]
 #[serde(untagged)]
-pub enum JsIcon<'a> {
+pub enum JsImage<'a> {
   Path(std::path::PathBuf),
   Bytes(&'a [u8]),
+  Resource(ResourceId),
   Rgba {
     rgba: &'a [u8],
     width: u32,
@@ -166,23 +199,24 @@ pub enum JsIcon<'a> {
   },
 }
 
-#[cfg(desktop)]
-impl<'a> TryFrom<JsIcon<'a>> for Image<'a> {
-  type Error = crate::Error;
-
-  fn try_from(img: JsIcon<'a>) -> Result<Self, Self::Error> {
-    match img {
+impl<'a> JsImage<'a> {
+  pub fn into_img<R: Runtime, M: Manager<R>>(self, app: &M) -> crate::Result<Arc<Image<'a>>> {
+    match self {
+      Self::Resource(rid) => {
+        let resources_table = app.resources_table();
+        resources_table.get::<Image<'static>>(rid)
+      }
       #[cfg(any(feature = "image-ico", feature = "image-png"))]
-      JsIcon::Path(path) => Self::from_path(path).map_err(Into::into),
+      Self::Path(path) => Image::from_path(path).map(Arc::new).map_err(Into::into),
 
       #[cfg(any(feature = "image-ico", feature = "image-png"))]
-      JsIcon::Bytes(bytes) => Self::from_bytes(bytes).map_err(Into::into),
+      Self::Bytes(bytes) => Image::from_bytes(bytes).map(Arc::new).map_err(Into::into),
 
-      JsIcon::Rgba {
+      Self::Rgba {
         rgba,
         width,
         height,
-      } => Ok(Self::new(rgba, width, height)),
+      } => Ok(Arc::new(Image::new(rgba, width, height))),
 
       #[cfg(not(any(feature = "image-ico", feature = "image-png")))]
       _ => Err(
@@ -190,9 +224,9 @@ impl<'a> TryFrom<JsIcon<'a>> for Image<'a> {
           ErrorKind::InvalidInput,
           format!(
             "expected RGBA image data, found {}",
-            match img {
-              JsIcon::Path(_) => "a file path",
-              JsIcon::Bytes(_) => "raw bytes",
+            match self {
+              JsImage::Path(_) => "a file path",
+              JsImage::Bytes(_) => "raw bytes",
               _ => unreachable!(),
             }
           ),

+ 116 - 0
core/tauri/src/image/plugin.rs

@@ -0,0 +1,116 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::plugin::{Builder, TauriPlugin};
+use crate::{command, AppHandle, Image, Manager, ResourceId, Runtime};
+
+#[command(root = "crate")]
+fn new<R: Runtime>(
+  app: AppHandle<R>,
+  rgba: Vec<u8>,
+  width: u32,
+  height: u32,
+) -> crate::Result<ResourceId> {
+  let image = Image::new_owned(rgba, width, height);
+  let mut resources_table = app.resources_table();
+  let rid = resources_table.add(image);
+  Ok(rid)
+}
+
+#[cfg(any(feature = "image-ico", feature = "image-png"))]
+#[command(root = "crate")]
+fn from_bytes<R: Runtime>(app: AppHandle<R>, bytes: Vec<u8>) -> crate::Result<ResourceId> {
+  let image = Image::from_bytes(&bytes)?.to_owned();
+  let mut resources_table = app.resources_table();
+  let rid = resources_table.add(image);
+  Ok(rid)
+}
+
+#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
+#[command(root = "crate")]
+fn from_bytes() -> std::result::Result<(), &'static str> {
+  Err("from_bytes is only supported if the `image-ico` or `image-png` Cargo features are enabled")
+}
+
+#[cfg(feature = "image-ico")]
+#[command(root = "crate")]
+fn from_ico_bytes<R: Runtime>(app: AppHandle<R>, bytes: Vec<u8>) -> crate::Result<ResourceId> {
+  let image = Image::from_ico_bytes(&bytes)?.to_owned();
+  let mut resources_table = app.resources_table();
+  let rid = resources_table.add(image);
+  Ok(rid)
+}
+
+#[cfg(not(feature = "image-ico"))]
+#[command(root = "crate")]
+fn from_ico_bytes() -> std::result::Result<(), &'static str> {
+  Err("from_ico_bytes is only supported if the `image-ico` Cargo feature is enabled")
+}
+
+#[cfg(feature = "image-png")]
+#[command(root = "crate")]
+fn from_png_bytes<R: Runtime>(app: AppHandle<R>, bytes: Vec<u8>) -> crate::Result<ResourceId> {
+  let image = Image::from_png_bytes(&bytes)?.to_owned();
+  let mut resources_table = app.resources_table();
+  let rid = resources_table.add(image);
+  Ok(rid)
+}
+
+#[cfg(not(feature = "image-png"))]
+#[command(root = "crate")]
+fn from_png_bytes() -> std::result::Result<(), &'static str> {
+  Err("from_png_bytes is only supported if the `image-ico` Cargo feature is enabled")
+}
+
+#[cfg(any(feature = "image-ico", feature = "image-png"))]
+#[command(root = "crate")]
+fn from_path<R: Runtime>(app: AppHandle<R>, path: std::path::PathBuf) -> crate::Result<ResourceId> {
+  let image = Image::from_path(path)?.to_owned();
+  let mut resources_table = app.resources_table();
+  let rid = resources_table.add(image);
+  Ok(rid)
+}
+
+#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
+#[command(root = "crate")]
+fn from_path() -> std::result::Result<(), &'static str> {
+  Err("from_path is only supported if the `image-ico` or `image-png` Cargo features are enabled")
+}
+
+#[command(root = "crate")]
+fn rgba<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<Vec<u8>> {
+  let resources_table = app.resources_table();
+  let image = resources_table.get::<Image<'_>>(rid)?;
+  Ok(image.rgba().to_vec())
+}
+
+#[command(root = "crate")]
+fn width<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<u32> {
+  let resources_table = app.resources_table();
+  let image = resources_table.get::<Image<'_>>(rid)?;
+  Ok(image.width())
+}
+
+#[command(root = "crate")]
+fn height<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> crate::Result<u32> {
+  let resources_table = app.resources_table();
+  let image = resources_table.get::<Image<'_>>(rid)?;
+  Ok(image.height())
+}
+
+/// Initializes the plugin.
+pub fn init<R: Runtime>() -> TauriPlugin<R> {
+  Builder::new("image")
+    .invoke_handler(crate::generate_handler![
+      new,
+      from_bytes,
+      from_ico_bytes,
+      from_png_bytes,
+      from_path,
+      rgba,
+      width,
+      height
+    ])
+    .build()
+}

+ 24 - 23
core/tauri/src/menu/plugin.rs

@@ -13,7 +13,7 @@ use tauri_runtime::window::dpi::Position;
 use super::{sealed::ContextMenuBase, *};
 use crate::{
   command,
-  image::JsIcon,
+  image::JsImage,
   ipc::{channel::JavaScriptChannelId, Channel},
   plugin::{Builder, TauriPlugin},
   resources::{ResourceId, ResourceTable},
@@ -46,29 +46,30 @@ pub(crate) struct AboutMetadata<'a> {
   pub website_label: Option<String>,
   pub credits: Option<String>,
   #[serde(borrow)]
-  pub icon: Option<JsIcon<'a>>,
+  pub icon: Option<JsImage<'a>>,
 }
 
-impl<'a> TryFrom<AboutMetadata<'a>> for super::AboutMetadata<'a> {
-  type Error = crate::Error;
-
-  fn try_from(value: AboutMetadata<'a>) -> Result<Self, Self::Error> {
-    let icon = match value.icon {
-      Some(i) => Some(i.try_into()?),
+impl<'a> AboutMetadata<'a> {
+  pub fn into_metdata<R: Runtime, M: Manager<R>>(
+    self,
+    app: &M,
+  ) -> crate::Result<super::AboutMetadata<'a>> {
+    let icon = match self.icon {
+      Some(i) => Some(i.into_img(app)?.as_ref().clone()),
       None => None,
     };
 
-    Ok(Self {
-      name: value.name,
-      version: value.version,
-      short_version: value.short_version,
-      authors: value.authors,
-      comments: value.comments,
-      copyright: value.copyright,
-      license: value.license,
-      website: value.website,
-      website_label: value.website_label,
-      credits: value.credits,
+    Ok(super::AboutMetadata {
+      name: self.name,
+      version: self.version,
+      short_version: self.short_version,
+      authors: self.authors,
+      comments: self.comments,
+      copyright: self.copyright,
+      license: self.license,
+      website: self.website,
+      website_label: self.website_label,
+      credits: self.credits,
       icon,
     })
   }
@@ -173,7 +174,7 @@ impl CheckMenuItemPayload {
 enum Icon<'a> {
   Native(NativeIcon),
   #[serde(borrow)]
-  Icon(JsIcon<'a>),
+  Icon(JsImage<'a>),
 }
 
 #[derive(Deserialize)]
@@ -203,7 +204,7 @@ impl<'a> IconMenuItemPayload<'a> {
     }
     builder = match self.icon {
       Icon::Native(native_icon) => builder.native_icon(native_icon),
-      Icon::Icon(icon) => builder.icon(icon.try_into()?),
+      Icon::Icon(icon) => builder.icon(icon.into_img(webview)?.as_ref().clone()),
     };
 
     let item = builder.build(webview)?;
@@ -291,7 +292,7 @@ impl<'a> PredefinedMenuItemPayload<'a> {
       Predefined::Quit => PredefinedMenuItem::quit(webview, self.text.as_deref()),
       Predefined::About(metadata) => {
         let metadata = match metadata {
-          Some(m) => Some(m.try_into()?),
+          Some(m) => Some(m.into_metdata(webview)?),
           None => None,
         };
         PredefinedMenuItem::about(webview, self.text.as_deref(), metadata)
@@ -852,7 +853,7 @@ fn set_icon<R: Runtime>(
 
   match icon {
     Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)),
-    Some(Icon::Icon(icon)) => icon_item.set_icon(Some(icon.try_into()?)),
+    Some(Icon::Icon(icon)) => icon_item.set_icon(Some(icon.into_img(&app)?.as_ref().clone())),
     None => {
       icon_item.set_icon(None)?;
       icon_item.set_native_icon(None)?;

+ 5 - 5
core/tauri/src/tray/plugin.rs

@@ -8,7 +8,7 @@ use serde::Deserialize;
 
 use crate::{
   command,
-  image::JsIcon,
+  image::JsImage,
   ipc::Channel,
   menu::{plugin::ItemKind, Menu, Submenu},
   plugin::{Builder, TauriPlugin},
@@ -25,7 +25,7 @@ struct TrayIconOptions<'a> {
   id: Option<String>,
   menu: Option<(ResourceId, ItemKind)>,
   #[serde(borrow)]
-  icon: Option<JsIcon<'a>>,
+  icon: Option<JsImage<'a>>,
   tooltip: Option<String>,
   title: Option<String>,
   temp_dir_path: Option<PathBuf>,
@@ -65,7 +65,7 @@ fn new<R: Runtime>(
     };
   }
   if let Some(icon) = options.icon {
-    builder = builder.icon(icon.try_into()?);
+    builder = builder.icon(icon.into_img(&app)?.as_ref().clone());
   }
   if let Some(tooltip) = options.tooltip {
     builder = builder.tooltip(tooltip);
@@ -94,12 +94,12 @@ fn new<R: Runtime>(
 fn set_icon<R: Runtime>(
   app: AppHandle<R>,
   rid: ResourceId,
-  icon: Option<JsIcon<'_>>,
+  icon: Option<JsImage<'_>>,
 ) -> crate::Result<()> {
   let resources_table = app.resources_table();
   let tray = resources_table.get::<TrayIcon<R>>(rid)?;
   let icon = match icon {
-    Some(i) => Some(i.try_into()?),
+    Some(i) => Some(i.into_img(&app)?.as_ref().clone()),
     None => None,
   };
   tray.set_icon(icon)

+ 4 - 3
core/tauri/src/window/plugin.rs

@@ -134,10 +134,11 @@ mod desktop_commands {
   pub async fn set_icon<R: Runtime>(
     window: Window<R>,
     label: Option<String>,
-    value: crate::image::JsIcon<'_>,
+    value: crate::image::JsImage<'_>,
   ) -> crate::Result<()> {
-    get_window(window, label)?
-      .set_icon(value.try_into()?)
+    let window = get_window(window, label)?;
+    window
+      .set_icon(value.into_img(&window)?.as_ref().clone())
       .map_err(Into::into)
   }
 

+ 0 - 7
examples/api/src-tauri/Cargo.lock

@@ -2975,12 +2975,6 @@ dependencies = [
  "loom",
 ]
 
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
 [[package]]
 name = "string_cache"
 version = "0.8.7"
@@ -3182,7 +3176,6 @@ dependencies = [
  "serde_repr",
  "serialize-to-javascript",
  "state",
- "static_assertions",
  "swift-rs",
  "tauri-build",
  "tauri-macros",

+ 3 - 5
examples/api/src-tauri/capabilities/run-app.json

@@ -2,10 +2,7 @@
   "$schema": "../gen/schemas/desktop-schema.json",
   "identifier": "run-app",
   "description": "permissions to run the app",
-  "windows": [
-    "main",
-    "main-*"
-  ],
+  "windows": ["main", "main-*"],
   "permissions": [
     {
       "identifier": "allow-log-operation",
@@ -24,6 +21,7 @@
     "window:default",
     "app:default",
     "resources:default",
+    "image:default",
     "menu:default",
     "tray:default",
     "app:allow-app-hide",
@@ -98,4 +96,4 @@
     "tray:allow-set-icon-as-template",
     "tray:allow-set-show-menu-on-left-click"
   ]
-}
+}

+ 141 - 0
tooling/api/src/image.ts

@@ -0,0 +1,141 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+import { Resource, invoke } from './core'
+
+/** An RGBA Image in row-major order from top to bottom. */
+export class Image extends Resource {
+  private constructor(rid: number) {
+    super(rid)
+  }
+
+  /** Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. */
+  static async new(
+    rgba: number[] | Uint8Array | ArrayBuffer,
+    width: number,
+    height: number
+  ): Promise<Image> {
+    return invoke<number>('plugin:image|new', {
+      rgba: transformImage(rgba),
+      width,
+      height
+    }).then((rid) => new Image(rid))
+  }
+
+  /**
+   * Creates a new image using the provided bytes by inferring the file format.
+   * If the format is known, prefer [@link Image.fromPngBytes] or [@link Image.fromIcoBytes].
+   *
+   * Only `ico` and `png` are supported (based on activated feature flag).
+   *
+   * Note that you need the `image-ico` or `image-png` Cargo features to use this API.
+   * To enable it, change your Cargo.toml file:
+   * ```toml
+   * [dependencies]
+   * tauri = { version = "...", features = ["...", "image-png"] }
+   * ```
+   */
+  static async fromBytes(
+    bytes: number[] | Uint8Array | ArrayBuffer
+  ): Promise<Image> {
+    return invoke<number>('plugin:image|from_bytes', {
+      bytes: transformImage(bytes)
+    }).then((rid) => new Image(rid))
+  }
+
+  /**
+   * Creates a new image using the provided png bytes.
+   *
+   * Note that you need the `image-png` Cargo features to use this API.
+   * To enable it, change your Cargo.toml file:
+   * ```toml
+   * [dependencies]
+   * tauri = { version = "...", features = ["...", "image-png"] }
+   * ```
+   */
+  static async fromPngBytes(
+    bytes: number[] | Uint8Array | ArrayBuffer
+  ): Promise<Image> {
+    return invoke<number>('plugin:image|from_png_bytes', {
+      bytes: transformImage(bytes)
+    }).then((rid) => new Image(rid))
+  }
+
+  /**
+   * Creates a new image using the provided ico bytes.
+   *
+   * Note that you need the `image-ico` Cargo features to use this API.
+   * To enable it, change your Cargo.toml file:
+   * ```toml
+   * [dependencies]
+   * tauri = { version = "...", features = ["...", "image-ico"] }
+   * ```
+   */
+  static async fromIcoBytes(
+    bytes: number[] | Uint8Array | ArrayBuffer
+  ): Promise<Image> {
+    return invoke<number>('plugin:image|from_ico_bytes', {
+      bytes: transformImage(bytes)
+    }).then((rid) => new Image(rid))
+  }
+
+  /**
+   * Creates a new image using the provided path.
+   *
+   * Only `ico` and `png` are supported (based on activated feature flag).
+   *
+   * Note that you need the `image-ico` or `image-png` Cargo features to use this API.
+   * To enable it, change your Cargo.toml file:
+   * ```toml
+   * [dependencies]
+   * tauri = { version = "...", features = ["...", "image-png"] }
+   * ```
+   */
+  static async fromPath(path: string): Promise<Image> {
+    return invoke<number>('plugin:image|from_path', { path }).then(
+      (rid) => new Image(rid)
+    )
+  }
+
+  /** Returns the RGBA data for this image, in row-major order from top to bottom.  */
+  async rgba(): Promise<ArrayBuffer | number[]> {
+    return invoke<ArrayBuffer | number[]>('plugin:image|rgba', {
+      rid: this.rid
+    })
+  }
+
+  /** Returns the width of this image.  */
+  async width() {
+    return invoke<number>('plugin:image|width', { rid: this.rid })
+  }
+
+  /** Returns the height of this image. */
+  async height() {
+    return invoke<number>('plugin:image|height', { rid: this.rid })
+  }
+}
+
+/**
+ * Transforms image from various types into a type acceptable by Rust. Intended for internal use only.
+ *
+ * @ignore
+ */
+export function transformImage<T>(
+  image: string | Image | Uint8Array | ArrayBuffer | number[] | null
+): T {
+  const ret =
+    image == null
+      ? null
+      : typeof image === 'string'
+        ? image
+        : image instanceof Uint8Array
+          ? Array.from(image)
+          : image instanceof ArrayBuffer
+            ? Array.from(new Uint8Array(image))
+            : image instanceof Image
+              ? image.rid
+              : image
+
+  return ret as T
+}

+ 3 - 1
tooling/api/src/index.ts

@@ -23,6 +23,7 @@ import * as path from './path'
 import * as dpi from './dpi'
 import * as tray from './tray'
 import * as menu from './menu'
+import * as image from './image'
 
 export {
   app,
@@ -34,5 +35,6 @@ export {
   webview,
   webviewWindow,
   tray,
-  menu
+  menu,
+  image
 }

+ 10 - 0
tooling/api/src/menu/base.ts

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: MIT
 
 import { Channel, invoke, Resource } from '../core'
+import { transformImage } from '../image'
 import { CheckMenuItemOptions } from './checkMenuItem'
 import { IconMenuItemOptions } from './iconMenuItem'
 import { MenuItemOptions } from './menuItem'
@@ -76,6 +77,15 @@ export async function newMenu(
         if ('rid' in i) {
           return [i.rid, i.kind]
         }
+
+        if ('item' in i && typeof i.item === 'object' && i.item.About?.icon) {
+          i.item.About.icon = transformImage(i.item.About.icon)
+        }
+
+        if ('icon' in i && i.icon) {
+          i.icon = transformImage(i.icon)
+        }
+
         return injectChannel(i)
       })
     }

+ 16 - 3
tooling/api/src/menu/iconMenuItem.ts

@@ -5,6 +5,7 @@
 import { MenuItemBase, newMenu } from './base'
 import { type MenuItemOptions } from '../menu'
 import { invoke } from '../core'
+import { Image, transformImage } from '../image'
 
 /**
  * A native Icon to be used for the menu item
@@ -133,7 +134,7 @@ export interface IconMenuItemOptions extends MenuItemOptions {
   /**
    * Icon to be used for the new icon menu item.
    */
-  icon?: NativeIcon | string | Uint8Array
+  icon?: NativeIcon | string | Image | Uint8Array | ArrayBuffer | number[]
 }
 
 /**
@@ -189,7 +190,19 @@ export class IconMenuItem extends MenuItemBase {
   }
 
   /** Sets an icon for this icon menu item */
-  async setIcon(icon: NativeIcon | string | Uint8Array | null): Promise<void> {
-    return invoke('plugin:menu|set_icon', { rid: this.rid, icon })
+  async setIcon(
+    icon:
+      | NativeIcon
+      | string
+      | Image
+      | Uint8Array
+      | ArrayBuffer
+      | number[]
+      | null
+  ): Promise<void> {
+    return invoke('plugin:menu|set_icon', {
+      rid: this.rid,
+      icon: transformImage(icon)
+    })
   }
 }

+ 2 - 1
tooling/api/src/menu/predefinedMenuItem.ts

@@ -4,6 +4,7 @@
 
 import { MenuItemBase, newMenu } from './base'
 import { invoke } from '../core'
+import { Image } from '../image'
 
 /** A metadata for the about predefined menu item. */
 export interface AboutMetadata {
@@ -76,7 +77,7 @@ export interface AboutMetadata {
    *
    * - **Windows:** Unsupported.
    */
-  icon?: string | Uint8Array
+  icon?: string | Uint8Array | ArrayBuffer | number[] | Image
 }
 
 /** Options for creating a new predefined menu item. */

+ 17 - 8
tooling/api/src/tray.ts

@@ -4,6 +4,7 @@
 
 import type { Menu, Submenu } from './menu'
 import { Channel, invoke, Resource } from './core'
+import { Image, transformImage } from './image'
 
 /**
  * Describes a tray event emitted when a tray icon is clicked
@@ -58,7 +59,7 @@ export interface TrayIconOptions {
    * tauri = { version = "...", features = ["...", "image-png"] }
    * ```
    */
-  icon?: string | Uint8Array | number[]
+  icon?: string | Uint8Array | ArrayBuffer | number[] | Image
   /** The tray icon tooltip */
   tooltip?: string
   /**
@@ -132,10 +133,7 @@ export class TrayIcon extends Resource {
       options.menu = [options.menu.rid, options.menu.kind]
     }
     if (options?.icon) {
-      options.icon =
-        typeof options.icon === 'string'
-          ? options.icon
-          : Array.from(options.icon)
+      options.icon = transformImage(options.icon)
     }
 
     const handler = new Channel<TrayIconEvent>()
@@ -150,11 +148,22 @@ export class TrayIcon extends Resource {
     }).then(([rid, id]) => new TrayIcon(rid, id))
   }
 
-  /** Sets a new tray icon. If `null` is provided, it will remove the icon. */
-  async setIcon(icon: string | Uint8Array | null): Promise<void> {
+  /**
+   *  Sets a new tray icon. If `null` is provided, it will remove the icon.
+   *
+   * Note that you need the `image-ico` or `image-png` Cargo features to use this API.
+   * To enable it, change your Cargo.toml file:
+   * ```toml
+   * [dependencies]
+   * tauri = { version = "...", features = ["...", "image-png"] }
+   * ```
+   */
+  async setIcon(
+    icon: string | Image | Uint8Array | ArrayBuffer | number[] | null
+  ): Promise<void> {
     let trayIcon = null
     if (icon) {
-      trayIcon = typeof icon === 'string' ? icon : Array.from(icon)
+      trayIcon = transformImage(icon)
     }
     return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon })
   }

+ 5 - 2
tooling/api/src/window.ts

@@ -36,6 +36,7 @@ import {
 import { invoke } from './core'
 import { WebviewWindow } from './webviewWindow'
 import type { FileDropEvent, FileDropPayload } from './webview'
+import { Image, transformImage } from './image'
 
 /**
  * Allows you to retrieve information about a given monitor.
@@ -1393,10 +1394,12 @@ class Window {
    * @param icon Icon bytes or path to the icon file.
    * @returns A promise indicating the success or failure of the operation.
    */
-  async setIcon(icon: string | Uint8Array): Promise<void> {
+  async setIcon(
+    icon: string | Image | Uint8Array | ArrayBuffer | number[]
+  ): Promise<void> {
     return invoke('plugin:window|set_icon', {
       label: this.label,
-      value: typeof icon === 'string' ? icon : Array.from(icon)
+      value: transformImage(icon)
     })
   }
 

+ 1 - 0
tooling/cli/templates/app/src-tauri/capabilities/default.json

@@ -10,6 +10,7 @@
     "webview:default",
     "app:default",
     "resources:default",
+    "image:default",
     "menu:default",
     "tray:default"
   ]

Some files were not shown because too many files changed in this diff