Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add include_image macro #9959

Merged
merged 18 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changes/include-image-macro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"tauri": "patch:feat"
"tauri-utils": "patch:feat"
"tauri-macros": "patch:feat"
"tauri-codegen": "patch:feat"
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
"tauri-cli": "patch:feat"
"@tauri-apps/cli": "patch:feat"
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
---

add a macro `include_image` to help using images from rust api directly like this `TrayIconBuilder::new().icon(include_image!("./icons/32x32.png")).build().unwrap();`
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
159 changes: 20 additions & 139 deletions core/tauri-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -223,15 +216,15 @@ 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,
&config_parent,
|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
Expand All @@ -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)))?
}
};

Expand All @@ -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)
};
Expand Down Expand Up @@ -289,17 +282,9 @@ 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`."
))
match image_icon(&root, &out_dir, &tray_icon_icon_path) {
Ok(i) => quote!(context.set_tray_icon(Some(#i));),
Err(e) => return Err(e),
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
} else {
quote!()
Expand Down Expand Up @@ -504,122 +489,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 changes: 16 additions & 0 deletions core/tauri-codegen/src/embedded_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
InvalidImageExtension { extension: PathBuf, path: PathBuf },

#[error("failed to walk directory {path} because {error}")]
Walkdir {
path: PathBuf,
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Loading
Loading