Просмотр исходного кода

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

Tony 1 год назад
Родитель
Сommit
5b769948a8

+ 5 - 0
.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.

+ 7 - 0
.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.

+ 19 - 140
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<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)
 }

+ 16 - 0
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<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)
+}

+ 136 - 0
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<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)
+}

+ 2 - 0
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;
 

+ 1 - 1
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": {

+ 59 - 1
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::<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()
+}

+ 4 - 0
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<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.

+ 1 - 0
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};

+ 1 - 1
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": {