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>
This commit is contained in:
Amr Bashir 2024-03-03 04:31:08 +02:00 committed by GitHub
parent 2ca9afb576
commit 77b9a508a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 432 additions and 83 deletions

View File

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

7
Cargo.lock generated
View File

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

View File

@ -72,7 +72,6 @@ png = { version = "0.17", optional = true }
ico = { version = "0.3.0", optional = true } ico = { version = "0.3.0", optional = true }
http-range = { version = "0.1.5", optional = true } http-range = { version = "0.1.5", optional = true }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
static_assertions = "1"
heck = "0.4" 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] [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]

View File

@ -137,6 +137,19 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("app_hide", false), ("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)]), ("resources", &[("close", true)]),
( (
"menu", "menu",

View File

@ -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 one or more lines are too long

View File

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

View File

@ -2,8 +2,13 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pub mod plugin;
use std::borrow::Cow; use std::borrow::Cow;
use std::io::{Error, ErrorKind}; 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. /// An RGBA Image in row-major order from top to bottom.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -13,6 +18,21 @@ pub struct Image<'a> {
height: u32, 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> { 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. /// 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 { 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 { pub fn height(&self) -> u32 {
self.height 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> { 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)] #[derive(serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum JsIcon<'a> { pub enum JsImage<'a> {
Path(std::path::PathBuf), Path(std::path::PathBuf),
Bytes(&'a [u8]), Bytes(&'a [u8]),
Resource(ResourceId),
Rgba { Rgba {
rgba: &'a [u8], rgba: &'a [u8],
width: u32, width: u32,
@ -166,23 +199,24 @@ pub enum JsIcon<'a> {
}, },
} }
#[cfg(desktop)] impl<'a> JsImage<'a> {
impl<'a> TryFrom<JsIcon<'a>> for Image<'a> { pub fn into_img<R: Runtime, M: Manager<R>>(self, app: &M) -> crate::Result<Arc<Image<'a>>> {
type Error = crate::Error; match self {
Self::Resource(rid) => {
fn try_from(img: JsIcon<'a>) -> Result<Self, Self::Error> { let resources_table = app.resources_table();
match img { resources_table.get::<Image<'static>>(rid)
}
#[cfg(any(feature = "image-ico", feature = "image-png"))] #[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"))] #[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, rgba,
width, width,
height, height,
} => Ok(Self::new(rgba, width, height)), } => Ok(Arc::new(Image::new(rgba, width, height))),
#[cfg(not(any(feature = "image-ico", feature = "image-png")))] #[cfg(not(any(feature = "image-ico", feature = "image-png")))]
_ => Err( _ => Err(
@ -190,9 +224,9 @@ impl<'a> TryFrom<JsIcon<'a>> for Image<'a> {
ErrorKind::InvalidInput, ErrorKind::InvalidInput,
format!( format!(
"expected RGBA image data, found {}", "expected RGBA image data, found {}",
match img { match self {
JsIcon::Path(_) => "a file path", JsImage::Path(_) => "a file path",
JsIcon::Bytes(_) => "raw bytes", JsImage::Bytes(_) => "raw bytes",
_ => unreachable!(), _ => unreachable!(),
} }
), ),

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

141
tooling/api/src/image.ts Normal file
View File

@ -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
}

View File

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

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Channel, invoke, Resource } from '../core' import { Channel, invoke, Resource } from '../core'
import { transformImage } from '../image'
import { CheckMenuItemOptions } from './checkMenuItem' import { CheckMenuItemOptions } from './checkMenuItem'
import { IconMenuItemOptions } from './iconMenuItem' import { IconMenuItemOptions } from './iconMenuItem'
import { MenuItemOptions } from './menuItem' import { MenuItemOptions } from './menuItem'
@ -76,6 +77,15 @@ export async function newMenu(
if ('rid' in i) { if ('rid' in i) {
return [i.rid, i.kind] 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) return injectChannel(i)
}) })
} }

View File

@ -5,6 +5,7 @@
import { MenuItemBase, newMenu } from './base' import { MenuItemBase, newMenu } from './base'
import { type MenuItemOptions } from '../menu' import { type MenuItemOptions } from '../menu'
import { invoke } from '../core' import { invoke } from '../core'
import { Image, transformImage } from '../image'
/** /**
* A native Icon to be used for the menu item * 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 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 */ /** Sets an icon for this icon menu item */
async setIcon(icon: NativeIcon | string | Uint8Array | null): Promise<void> { async setIcon(
return invoke('plugin:menu|set_icon', { rid: this.rid, icon }) icon:
| NativeIcon
| string
| Image
| Uint8Array
| ArrayBuffer
| number[]
| null
): Promise<void> {
return invoke('plugin:menu|set_icon', {
rid: this.rid,
icon: transformImage(icon)
})
} }
} }

View File

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

View File

@ -4,6 +4,7 @@
import type { Menu, Submenu } from './menu' import type { Menu, Submenu } from './menu'
import { Channel, invoke, Resource } from './core' import { Channel, invoke, Resource } from './core'
import { Image, transformImage } from './image'
/** /**
* Describes a tray event emitted when a tray icon is clicked * Describes a tray event emitted when a tray icon is clicked
@ -58,7 +59,7 @@ export interface TrayIconOptions {
* tauri = { version = "...", features = ["...", "image-png"] } * tauri = { version = "...", features = ["...", "image-png"] }
* ``` * ```
*/ */
icon?: string | Uint8Array | number[] icon?: string | Uint8Array | ArrayBuffer | number[] | Image
/** The tray icon tooltip */ /** The tray icon tooltip */
tooltip?: string tooltip?: string
/** /**
@ -132,10 +133,7 @@ export class TrayIcon extends Resource {
options.menu = [options.menu.rid, options.menu.kind] options.menu = [options.menu.rid, options.menu.kind]
} }
if (options?.icon) { if (options?.icon) {
options.icon = options.icon = transformImage(options.icon)
typeof options.icon === 'string'
? options.icon
: Array.from(options.icon)
} }
const handler = new Channel<TrayIconEvent>() const handler = new Channel<TrayIconEvent>()
@ -150,11 +148,22 @@ export class TrayIcon extends Resource {
}).then(([rid, id]) => new TrayIcon(rid, id)) }).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 let trayIcon = null
if (icon) { if (icon) {
trayIcon = typeof icon === 'string' ? icon : Array.from(icon) trayIcon = transformImage(icon)
} }
return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon }) return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon })
} }

View File

@ -36,6 +36,7 @@ import {
import { invoke } from './core' import { invoke } from './core'
import { WebviewWindow } from './webviewWindow' import { WebviewWindow } from './webviewWindow'
import type { FileDropEvent, FileDropPayload } from './webview' import type { FileDropEvent, FileDropPayload } from './webview'
import { Image, transformImage } from './image'
/** /**
* Allows you to retrieve information about a given monitor. * 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. * @param icon Icon bytes or path to the icon file.
* @returns A promise indicating the success or failure of the operation. * @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', { return invoke('plugin:window|set_icon', {
label: this.label, label: this.label,
value: typeof icon === 'string' ? icon : Array.from(icon) value: transformImage(icon)
}) })
} }

View File

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