diff --git a/.changes/include-image-macro-codegen.md b/.changes/include-image-macro-codegen.md new file mode 100644 index 000000000..cff5f71ff --- /dev/null +++ b/.changes/include-image-macro-codegen.md @@ -0,0 +1,5 @@ +--- +"tauri-codegen": "patch:feat" +--- + +Add `include_image_codegen` function to help embedding instances of `Image` struct at compile-time in rust to be used with window, menu or tray icons. diff --git a/.changes/include-image-macro.md b/.changes/include-image-macro.md new file mode 100644 index 000000000..f68052710 --- /dev/null +++ b/.changes/include-image-macro.md @@ -0,0 +1,7 @@ +--- +"tauri": "patch:feat" +"tauri-utils": "patch:feat" +"tauri-macros": "patch:feat" +--- + +Add `include_image` macro to help embedding instances of `Image` struct at compile-time in rust to be used with window, menu or tray icons. diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index 45e6ea80f..60d92a2d2 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -25,7 +25,10 @@ use tauri_utils::platform::Target; use tauri_utils::plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH; use tauri_utils::tokens::{map_lit, str_lit}; -use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError}; +use crate::embedded_assets::{ + ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, +}; +use crate::image::{ico_icon, image_icon, png_icon, raw_icon}; const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; @@ -65,7 +68,7 @@ fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut Csp fn map_core_assets( options: &AssetOptions, -) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> Result<(), EmbeddedAssetsError> { +) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> EmbeddedAssetsResult<()> { let csp = options.csp; let dangerous_disable_asset_csp_modification = options.dangerous_disable_asset_csp_modification.clone(); @@ -92,7 +95,7 @@ fn map_core_assets( fn map_isolation( _options: &AssetOptions, dir: PathBuf, -) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> Result<(), EmbeddedAssetsError> { +) -> impl Fn(&AssetKey, &Path, &mut Vec, &mut CspHashes) -> EmbeddedAssetsResult<()> { // create the csp for the isolation iframe styling now, to make the runtime less complex let mut hasher = Sha256::new(); hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE); @@ -129,7 +132,7 @@ fn map_isolation( } /// Build a `tauri::Context` for including in application code. -pub fn context_codegen(data: ContextData) -> Result { +pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let ContextData { dev, config, @@ -201,17 +204,7 @@ pub fn context_codegen(data: ContextData) -> Result Result Result Result Result Result Result>( - root: &TokenStream, - out_dir: &Path, - path: P, -) -> Result { - let path = path.as_ref(); - let bytes = std::fs::read(path) - .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) - .to_vec(); - let icon_dir = ico::IconDir::read(std::io::Cursor::new(bytes)) - .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", path.display(), e)); - let entry = &icon_dir.entries()[0]; - let rgba = entry - .decode() - .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", path.display(), e)) - .rgba_data() - .to_vec(); - let width = entry.width(); - let height = entry.height(); - - let icon_file_name = path.file_name().unwrap(); - let out_path = out_dir.join(icon_file_name); - write_if_changed(&out_path, &rgba).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon_file_name = icon_file_name.to_str().unwrap(); - let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)), #width, #height)); - Ok(icon) -} - -fn raw_icon>(out_dir: &Path, path: P) -> Result { - let path = path.as_ref(); - let bytes = std::fs::read(path) - .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) - .to_vec(); - - let out_path = out_dir.join(path.file_name().unwrap()); - write_if_changed(&out_path, &bytes).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon_path = path.file_name().unwrap().to_str().unwrap().to_string(); - let icon = quote!(::std::option::Option::Some( - include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_path)).to_vec() - )); - Ok(icon) -} - -fn png_icon>( - root: &TokenStream, - out_dir: &Path, - path: P, -) -> Result { - let path = path.as_ref(); - let bytes = std::fs::read(path) - .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) - .to_vec(); - let decoder = png::Decoder::new(std::io::Cursor::new(bytes)); - let mut reader = decoder - .read_info() - .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)); - - let (color_type, _) = reader.output_color_type(); - - if color_type != png::ColorType::Rgba { - panic!("icon {} is not RGBA", path.display()); - } - - let mut buffer: Vec = Vec::new(); - while let Ok(Some(row)) = reader.next_row() { - buffer.extend(row.data()); - } - let width = reader.info().width; - let height = reader.info().height; - - let icon_file_name = path.file_name().unwrap(); - let out_path = out_dir.join(icon_file_name); - write_if_changed(&out_path, &buffer).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon_file_name = icon_file_name.to_str().unwrap(); - let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)), #width, #height)); - Ok(icon) -} - -fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> { - use std::fs::File; - use std::io::Write; - - if let Ok(curr) = std::fs::read(out_path) { - if curr == data { - return Ok(()); - } - } - - let mut out_file = File::create(out_path)?; - out_file.write_all(data) -} - -fn find_icon bool>( +fn find_icon( config: &Config, config_parent: &Path, - predicate: F, + predicate: impl Fn(&&String) -> bool, default: &str, ) -> PathBuf { let icon_path = config .bundle .icon .iter() - .find(|i| predicate(i)) - .cloned() - .unwrap_or_else(|| default.to_string()); + .find(predicate) + .map(AsRef::as_ref) + .unwrap_or(default); config_parent.join(icon_path) } diff --git a/core/tauri-codegen/src/embedded_assets.rs b/core/tauri-codegen/src/embedded_assets.rs index 1f22b1860..868635ae2 100644 --- a/core/tauri-codegen/src/embedded_assets.rs +++ b/core/tauri-codegen/src/embedded_assets.rs @@ -48,6 +48,9 @@ pub enum EmbeddedAssetsError { #[error("invalid prefix {prefix} used while including path {path}")] PrefixInvalid { prefix: PathBuf, path: PathBuf }, + #[error("invalid extension {extension} used for image {path}, must be `ico` or `png`")] + InvalidImageExtension { extension: PathBuf, path: PathBuf }, + #[error("failed to walk directory {path} because {error}")] Walkdir { path: PathBuf, @@ -61,6 +64,8 @@ pub enum EmbeddedAssetsError { Version(#[from] semver::Error), } +pub type EmbeddedAssetsResult = Result; + /// Represent a directory of assets that are compressed and embedded. /// /// This is the compile time generation of [`tauri_utils::assets::Assets`] from a directory. Assets @@ -439,3 +444,14 @@ impl ToTokens for EmbeddedAssets { }}); } } + +pub(crate) fn ensure_out_dir() -> EmbeddedAssetsResult { + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| EmbeddedAssetsError::OutDir) + .map(PathBuf::from) + .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?; + + // make sure that our output directory is created + std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?; + Ok(out_dir) +} diff --git a/core/tauri-codegen/src/image.rs b/core/tauri-codegen/src/image.rs new file mode 100644 index 000000000..62adbbca8 --- /dev/null +++ b/core/tauri-codegen/src/image.rs @@ -0,0 +1,136 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::embedded_assets::{ensure_out_dir, EmbeddedAssetsError, EmbeddedAssetsResult}; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; +use std::path::Path; +use syn::{punctuated::Punctuated, Ident, PathArguments, PathSegment, Token}; + +pub fn include_image_codegen(path: &Path) -> EmbeddedAssetsResult { + let out_dir = ensure_out_dir()?; + + let mut segments = Punctuated::new(); + segments.push(PathSegment { + ident: Ident::new("tauri", Span::call_site()), + arguments: PathArguments::None, + }); + let root = syn::Path { + leading_colon: Some(Token![::](Span::call_site())), + segments, + }; + + image_icon(&root.to_token_stream(), &out_dir, path) +} + +pub(crate) fn image_icon( + root: &TokenStream, + out_dir: &Path, + path: &Path, +) -> EmbeddedAssetsResult { + let extension = path.extension().unwrap_or_default(); + if extension == "ico" { + ico_icon(root, out_dir, path) + } else if extension == "png" { + png_icon(root, out_dir, path) + } else { + Err(EmbeddedAssetsError::InvalidImageExtension { + extension: extension.into(), + path: path.to_path_buf(), + }) + } +} + +pub(crate) fn raw_icon(out_dir: &Path, path: &Path) -> EmbeddedAssetsResult { + let bytes = + std::fs::read(path).unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)); + + let out_path = out_dir.join(path.file_name().unwrap()); + write_if_changed(&out_path, &bytes).map_err(|error| EmbeddedAssetsError::AssetWrite { + path: path.to_owned(), + error, + })?; + + let icon_path = path.file_name().unwrap().to_str().unwrap().to_string(); + let icon = quote!(::std::option::Option::Some( + include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_path)).to_vec() + )); + Ok(icon) +} + +pub(crate) fn ico_icon( + root: &TokenStream, + out_dir: &Path, + path: &Path, +) -> EmbeddedAssetsResult { + let file = std::fs::File::open(path) + .unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)); + let icon_dir = ico::IconDir::read(file) + .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", path.display(), e)); + let entry = &icon_dir.entries()[0]; + let rgba = entry + .decode() + .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", path.display(), e)) + .rgba_data() + .to_vec(); + let width = entry.width(); + let height = entry.height(); + + let icon_file_name = path.file_name().unwrap(); + let out_path = out_dir.join(icon_file_name); + write_if_changed(&out_path, &rgba).map_err(|error| EmbeddedAssetsError::AssetWrite { + path: path.to_owned(), + error, + })?; + + let icon_file_name = icon_file_name.to_str().unwrap(); + let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)), #width, #height)); + Ok(icon) +} + +pub(crate) fn png_icon( + root: &TokenStream, + out_dir: &Path, + path: &Path, +) -> EmbeddedAssetsResult { + let file = std::fs::File::open(path) + .unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)); + let decoder = png::Decoder::new(file); + let mut reader = decoder + .read_info() + .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)); + + let (color_type, _) = reader.output_color_type(); + + if color_type != png::ColorType::Rgba { + panic!("icon {} is not RGBA", path.display()); + } + + let mut buffer: Vec = Vec::new(); + while let Ok(Some(row)) = reader.next_row() { + buffer.extend(row.data()); + } + let width = reader.info().width; + let height = reader.info().height; + + let icon_file_name = path.file_name().unwrap(); + let out_path = out_dir.join(icon_file_name); + write_if_changed(&out_path, &buffer).map_err(|error| EmbeddedAssetsError::AssetWrite { + path: path.to_owned(), + error, + })?; + + let icon_file_name = icon_file_name.to_str().unwrap(); + let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #icon_file_name)), #width, #height)); + Ok(icon) +} + +pub(crate) fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> { + if let Ok(curr) = std::fs::read(out_path) { + if curr == data { + return Ok(()); + } + } + std::fs::write(out_path, data) +} diff --git a/core/tauri-codegen/src/lib.rs b/core/tauri-codegen/src/lib.rs index 59896a6d4..48a3033fd 100644 --- a/core/tauri-codegen/src/lib.rs +++ b/core/tauri-codegen/src/lib.rs @@ -13,6 +13,7 @@ )] pub use self::context::{context_codegen, ContextData}; +pub use self::image::include_image_codegen; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -22,6 +23,7 @@ use tauri_utils::platform::Target; mod context; pub mod embedded_assets; +mod image; #[doc(hidden)] pub mod vendor; diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 43578ca10..7207e69f1 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -1318,7 +1318,7 @@ ] }, "iconPath": { - "description": "Path to the default icon to use for the tray icon.", + "description": "Path to the default icon to use for the tray icon.\n\nNote: this stores the image in raw pixels to the final binary, so keep the icon size (width and height) small or else it's going to bloat your final executable", "type": "string" }, "iconAsTemplate": { diff --git a/core/tauri-macros/src/lib.rs b/core/tauri-macros/src/lib.rs index a5d55748d..656714cb0 100644 --- a/core/tauri-macros/src/lib.rs +++ b/core/tauri-macros/src/lib.rs @@ -11,9 +11,12 @@ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] +use std::path::PathBuf; + use crate::context::ContextItems; use proc_macro::TokenStream; -use syn::parse_macro_input; +use quote::quote; +use syn::{parse2, parse_macro_input, LitStr}; mod command; mod menu; @@ -151,3 +154,58 @@ pub fn do_menu_item(input: TokenStream) -> TokenStream { let tokens = parse_macro_input!(input as menu::DoMenuItemInput); menu::do_menu_item(tokens).into() } + +/// Convert a .png or .ico icon to an Image +/// for things like `tauri::tray::TrayIconBuilder` to consume, +/// relative paths are resolved from `CARGO_MANIFEST_DIR`, not current file +/// +/// ### Examples +/// +/// ```ignore +/// const APP_ICON: Image<'_> = include_image!("./icons/32x32.png"); +/// +/// // then use it with tray +/// TrayIconBuilder::new().icon(APP_ICON).build().unwrap(); +/// +/// // or with window +/// WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) +/// .icon(APP_ICON) +/// .unwrap() +/// .build() +/// .unwrap(); +/// +/// // or with any other functions that takes `Image` struct +/// ``` +/// +/// Note: this stores the image in raw pixels to the final binary, +/// so keep the icon size (width and height) small +/// or else it's going to bloat your final executable +#[proc_macro] +pub fn include_image(tokens: TokenStream) -> TokenStream { + let path = match parse2::(tokens.into()) { + Ok(path) => path, + Err(err) => return err.into_compile_error().into(), + }; + let path = PathBuf::from(path.value()); + let resolved_path = if path.is_relative() { + if let Ok(base_dir) = std::env::var("CARGO_MANIFEST_DIR").map(PathBuf::from) { + base_dir.join(path) + } else { + return quote!(compile_error!("$CARGO_MANIFEST_DIR is not defined")).into(); + } + } else { + path + }; + if !resolved_path.exists() { + let error_string = format!( + "Provided Image path \"{}\" doesn't exists", + resolved_path.display() + ); + return quote!(compile_error!(#error_string)).into(); + } + match tauri_codegen::include_image_codegen(&resolved_path).map_err(|error| error.to_string()) { + Ok(output) => output, + Err(error) => quote!(compile_error!(#error)), + } + .into() +} diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 7bb2fc01b..076baaec8 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -1798,6 +1798,10 @@ pub struct TrayIconConfig { /// Set an id for this tray icon so you can reference it later, defaults to `main`. pub id: Option, /// Path to the default icon to use for the tray icon. + /// + /// Note: this stores the image in raw pixels to the final binary, + /// so keep the icon size (width and height) small + /// or else it's going to bloat your final executable #[serde(alias = "icon-path")] pub icon_path: PathBuf, /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS. diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index ca09e1777..eabb02d9d 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -75,6 +75,7 @@ pub use resources::{Resource, ResourceId, ResourceTable}; #[cfg(target_os = "ios")] #[doc(hidden)] pub use swift_rs; +pub use tauri_macros::include_image; #[cfg(mobile)] pub use tauri_macros::mobile_entry_point; pub use tauri_macros::{command, generate_handler}; diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 43578ca10..7207e69f1 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -1318,7 +1318,7 @@ ] }, "iconPath": { - "description": "Path to the default icon to use for the tray icon.", + "description": "Path to the default icon to use for the tray icon.\n\nNote: this stores the image in raw pixels to the final binary, so keep the icon size (width and height) small or else it's going to bloat your final executable", "type": "string" }, "iconAsTemplate": {