feat(core): add include_image macro (#9959)

This commit is contained in:
Tony 2024-06-06 11:03:11 +08:00 committed by GitHub
parent 586a816e62
commit 5b769948a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 251 additions and 143 deletions

View File

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

View File

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

View File

@ -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<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &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<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &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<TokenStream, EmbeddedAssetsError> {
pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
let ContextData {
dev,
config,
@ -201,17 +204,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
quote!(#assets)
};
let out_dir = {
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)?;
out_dir
};
let out_dir = ensure_out_dir()?;
let default_window_icon = {
if target == Target::Windows {
@ -223,7 +216,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
"icons/icon.ico",
);
if icon_path.exists() {
ico_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
ico_icon(&root, &out_dir, &icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
} else {
let icon_path = find_icon(
&config,
@ -231,7 +224,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|i| i.ends_with(".png"),
"icons/icon.png",
);
png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
png_icon(&root, &out_dir, &icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
}
} else {
// handle default window icons for Unix targets
@ -241,7 +234,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|i| i.ends_with(".png"),
"icons/icon.png",
);
png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
png_icon(&root, &out_dir, &icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
}
};
@ -260,7 +253,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
"icons/icon.png",
);
}
raw_icon(&out_dir, icon_path)?
raw_icon(&out_dir, &icon_path)?
} else {
quote!(::std::option::Option::None)
};
@ -289,18 +282,8 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
let with_tray_icon_code = if target.is_desktop() {
if let Some(tray) = &config.app.tray_icon {
let tray_icon_icon_path = config_parent.join(&tray.icon_path);
let ext = tray_icon_icon_path.extension();
if ext.map_or(false, |e| e == "ico") {
ico_icon(&root, &out_dir, tray_icon_icon_path)
.map(|i| quote!(context.set_tray_icon(Some(#i));))?
} else if ext.map_or(false, |e| e == "png") {
png_icon(&root, &out_dir, tray_icon_icon_path)
.map(|i| quote!(context.set_tray_icon(Some(#i));))?
} else {
quote!(compile_error!(
"The tray icon extension must be either `.ico` or `.png`."
))
}
image_icon(&root, &out_dir, &tray_icon_icon_path)
.map(|i| quote!(context.set_tray_icon(Some(#i));))?
} else {
quote!()
}
@ -504,122 +487,18 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
}))
}
fn ico_icon<P: AsRef<Path>>(
root: &TokenStream,
out_dir: &Path,
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
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<P: AsRef<Path>>(out_dir: &Path, path: P) -> Result<TokenStream, EmbeddedAssetsError> {
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<P: AsRef<Path>>(
root: &TokenStream,
out_dir: &Path,
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
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<u8> = 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<F: Fn(&&String) -> 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)
}

View File

@ -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<T> = Result<T, EmbeddedAssetsError>;
/// 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<PathBuf> {
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)
}

View File

@ -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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<u8> = 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)
}

View File

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

View File

@ -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": {

View File

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

View File

@ -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<String>,
/// 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.

View File

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

View File

@ -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": {