From 50f7ccbbf3467f33cc7dd1cca53125fec6eda1c6 Mon Sep 17 00:00:00 2001 From: fetzsav <66761778+fetzsav@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:58:01 -0500 Subject: [PATCH] [feat (issue #6389)] make tauri icon support SVGs (#6444) Co-authored-by: Fetzer Co-authored-by: Lucas Nogueira Co-authored-by: FabianLars --- .changes/icon-svg.md | 6 + tooling/cli/Cargo.lock | 307 ++++++++++++++++++++++++++++++++++++++++ tooling/cli/Cargo.toml | 1 + tooling/cli/src/icon.rs | 120 ++++++++++++---- 4 files changed, 409 insertions(+), 25 deletions(-) create mode 100644 .changes/icon-svg.md diff --git a/.changes/icon-svg.md b/.changes/icon-svg.md new file mode 100644 index 000000000..6db33a222 --- /dev/null +++ b/.changes/icon-svg.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +Add suport to SVG input image for the `tauri icon` command. diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index ac5984a89..e4a19681a 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -155,6 +155,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-lock" version = "2.8.0" @@ -850,6 +862,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "data-url" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" + [[package]] name = "deranged" version = "0.3.9" @@ -1183,6 +1201,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "flume" version = "0.11.0" @@ -1198,6 +1222,29 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontconfig-parser" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "674e258f4b5d2dcd63888c01c68413c51f565e8af99d2f7701c7b81d79ef41c4" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1687,6 +1734,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "include_dir" version = "0.7.3" @@ -2042,6 +2095,15 @@ dependencies = [ "selectors", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2257,6 +2319,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -2945,6 +3016,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.3" @@ -3183,6 +3260,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3285,6 +3368,32 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "resvg" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7980f653f9a7db31acff916a262c3b78c562919263edea29bf41a056e20497" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.16.20" @@ -3312,6 +3421,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" +[[package]] +name = "roxmltree" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" +dependencies = [ + "xmlparser", +] + [[package]] name = "rpassword" version = "7.2.0" @@ -3395,6 +3513,22 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rustybuzz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cd15fef9112a1f94ac64b58d1e4628192631ad6af4dc69997f995459c874e7" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.15" @@ -3751,6 +3885,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3766,6 +3909,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -3851,6 +4003,15 @@ dependencies = [ "regex", ] +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + [[package]] name = "string_cache" version = "0.8.7" @@ -3963,6 +4124,16 @@ dependencies = [ "sval_fmt", ] +[[package]] +name = "svgtypes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71499ff2d42f59d26edb21369a308ede691421f79ebc0f001e2b1fd3a7c9e52" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "1.0.109" @@ -4119,6 +4290,7 @@ dependencies = [ "os_info", "os_pipe", "regex", + "resvg", "semver", "serde", "serde-value", @@ -4353,6 +4525,32 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b72a92a05db376db09fe6d50b7948d106011761c05a6a45e23e17ee9b556222" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac3865b9708fc7e1961a65c3a4fa55e984272f33092d3c859929f887fceb647" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -4553,6 +4751,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + [[package]] name = "tungstenite" version = "0.20.1" @@ -4599,6 +4803,18 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -4614,12 +4830,30 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f91c8b21fbbaa18853c3d0801c78f4fc94cdb976699bb03e832e75f7fd22f0" + +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.11" @@ -4671,6 +4905,67 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51daa774fe9ee5efcf7b4fec13019b8119cda764d9a8b5b06df02bb1445c656" +dependencies = [ + "base64 0.21.4", + "log", + "pico-args", + "usvg-parser", + "usvg-text-layout", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c88a5ffaa338f0e978ecf3d4e00d8f9f493e29bed0752e1a808a1db16afc40" +dependencies = [ + "data-url", + "flate2", + "imagesize", + "kurbo", + "log", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", + "usvg-tree", +] + +[[package]] +name = "usvg-text-layout" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d2374378cb7a3fb8f33894e0fdb8625e1bbc4f25312db8d91f862130b541593" +dependencies = [ + "fontdb", + "kurbo", + "log", + "rustybuzz", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cacb0c5edeaf3e80e5afcf5b0d4004cc1d36318befc9a7c6606507e5d0f4062" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -5205,6 +5500,18 @@ dependencies = [ "libc", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index eab58e0ed..5b2b27393 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -94,6 +94,7 @@ serde-value = "0.7.0" itertools = "0.11" local-ip-address = "0.5" css-color = "0.2" +resvg = "0.36.0" [target."cfg(windows)".dependencies] winapi = { version = "0.3", features = [ "handleapi", "processenv", "winbase", "wincon", "winnt" ] } diff --git a/tooling/cli/src/icon.rs b/tooling/cli/src/icon.rs index 2cfb74b2f..2d34b7714 100644 --- a/tooling/cli/src/icon.rs +++ b/tooling/cli/src/icon.rs @@ -23,6 +23,8 @@ use image::{ imageops::FilterType, open, ColorType, DynamicImage, ImageBuffer, ImageEncoder, Rgba, }; +use resvg::usvg::{fontdb, TreeParsing, TreeTextToPath}; +use resvg::{tiny_skia, usvg}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -41,7 +43,7 @@ struct PngEntry { #[derive(Debug, Parser)] #[clap(about = "Generate various icons for all major platforms")] pub struct Options { - /// Path to the source icon (png, 1024x1024px with transparency). + /// Path to the source icon (squared PNG or SVG file with transparency). #[clap(default_value = "./app-icon.png")] input: PathBuf, /// Output directory. @@ -58,6 +60,43 @@ pub struct Options { ios_color: String, } +enum Source { + Svg(resvg::Tree), + DynamicImage(DynamicImage), +} + +impl Source { + fn width(&self) -> u32 { + match self { + Self::Svg(svg) => svg.size.width() as u32, + Self::DynamicImage(i) => i.width(), + } + } + + fn height(&self) -> u32 { + match self { + Self::Svg(svg) => svg.size.height() as u32, + Self::DynamicImage(i) => i.height(), + } + } + + fn resize_exact(&self, size: u32) -> Result { + match self { + Self::Svg(svg) => { + let mut pixmap = tiny_skia::Pixmap::new(size, size).unwrap(); + let scale = size as f32 / svg.size.height(); + svg.render( + tiny_skia::Transform::from_scale(scale, scale), + &mut pixmap.as_mut(), + ); + let img_buffer = ImageBuffer::from_raw(size, size, pixmap.take()).unwrap(); + Ok(DynamicImage::ImageRgba8(img_buffer)) + } + Self::DynamicImage(i) => Ok(i.resize_exact(size, size, FilterType::Lanczos3)), + } + } +} + pub fn command(options: Options) -> Result<()> { let input = options.input; let out_dir = options.output.unwrap_or_else(|| tauri_dir().join("icons")); @@ -75,11 +114,37 @@ pub fn command(options: Options) -> Result<()> { create_dir_all(&out_dir).context("Can't create output directory")?; - let source = open(input) - .context("Can't read and decode source image")? - .into_rgba8(); + let source = if let Some(extension) = input.extension() { + if extension == "svg" { + let rtree = { + let opt = usvg::Options { + // Get file's absolute directory. + resources_dir: std::fs::canonicalize(&input) + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())), + ..Default::default() + }; - let source = DynamicImage::ImageRgba8(source); + let mut fontdb = fontdb::Database::new(); + fontdb.load_system_fonts(); + + let svg_data = std::fs::read(&input).unwrap(); + let mut tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); + tree.convert_text(&fontdb); + resvg::Tree::from_usvg(&tree) + }; + + Source::Svg(rtree) + } else { + Source::DynamicImage(DynamicImage::ImageRgba8( + open(&input) + .context("Can't read and decode source image")? + .into_rgba8(), + )) + } + } else { + panic!("Error loading image"); + }; if source.height() != source.width() { panic!("Source image must be square"); @@ -106,29 +171,29 @@ pub fn command(options: Options) -> Result<()> { .collect::>() { log::info!(action = "PNG"; "Creating {}", target.name); - resize_and_save_png(&source, target.size, &target.out_path)?; + resize_and_save_png(&source, target.size, &target.out_path, None)?; } } Ok(()) } -fn appx(source: &DynamicImage, out_dir: &Path) -> Result<()> { +fn appx(source: &Source, out_dir: &Path) -> Result<()> { log::info!(action = "Appx"; "Creating StoreLogo.png"); - resize_and_save_png(source, 50, &out_dir.join("StoreLogo.png"))?; + resize_and_save_png(source, 50, &out_dir.join("StoreLogo.png"), None)?; for size in [30, 44, 71, 89, 107, 142, 150, 284, 310] { let file_name = format!("Square{size}x{size}Logo.png"); log::info!(action = "Appx"; "Creating {}", file_name); - resize_and_save_png(source, size, &out_dir.join(&file_name))?; + resize_and_save_png(source, size, &out_dir.join(&file_name), None)?; } Ok(()) } // Main target: macOS -fn icns(source: &DynamicImage, out_dir: &Path) -> Result<()> { +fn icns(source: &Source, out_dir: &Path) -> Result<()> { log::info!(action = "ICNS"; "Creating icon.icns"); let entries: HashMap = serde_json::from_slice(include_bytes!("helpers/icns.json")).unwrap(); @@ -139,7 +204,7 @@ fn icns(source: &DynamicImage, out_dir: &Path) -> Result<()> { let size = entry.size; let mut buf = Vec::new(); - let image = source.resize_exact(size, size, FilterType::Lanczos3); + let image = source.resize_exact(size)?; write_png(image.as_bytes(), &mut buf, size)?; @@ -162,12 +227,12 @@ fn icns(source: &DynamicImage, out_dir: &Path) -> Result<()> { // Generate .ico file with layers for the most common sizes. // Main target: Windows -fn ico(source: &DynamicImage, out_dir: &Path) -> Result<()> { +fn ico(source: &Source, out_dir: &Path) -> Result<()> { log::info!(action = "ICO"; "Creating icon.ico"); let mut frames = Vec::new(); for size in [32, 16, 24, 48, 64, 256] { - let image = source.resize_exact(size, size, FilterType::Lanczos3); + let image = source.resize_exact(size)?; // Only the 256px layer can be compressed according to the ico specs. if size == 256 { @@ -196,7 +261,7 @@ fn ico(source: &DynamicImage, out_dir: &Path) -> Result<()> { // Generate .png files in 32x32, 128x128, 256x256, 512x512 (icon.png) // Main target: Linux -fn png(source: &DynamicImage, out_dir: &Path, ios_color: Rgba) -> Result<()> { +fn png(source: &Source, out_dir: &Path, ios_color: Rgba) -> Result<()> { fn desktop_entries(out_dir: &Path) -> Vec { let mut entries = Vec::new(); @@ -383,27 +448,32 @@ fn png(source: &DynamicImage, out_dir: &Path, ios_color: Rgba) -> Result<()> for entry in entries { log::info!(action = "PNG"; "Creating {}", entry.name); - resize_and_save_png(source, entry.size, &entry.out_path)?; + resize_and_save_png(source, entry.size, &entry.out_path, None)?; } - let source_rgba8 = source.as_rgba8().expect("unexpected image type"); - let mut img = ImageBuffer::from_fn(source_rgba8.width(), source_rgba8.height(), |_, _| { - ios_color - }); - image::imageops::overlay(&mut img, source_rgba8, 0, 0); - let image = DynamicImage::ImageRgba8(img); - for entry in ios_entries(&out)? { log::info!(action = "iOS"; "Creating {}", entry.name); - resize_and_save_png(&image, entry.size, &entry.out_path)?; + resize_and_save_png(source, entry.size, &entry.out_path, Some(ios_color))?; } Ok(()) } // Resize image and save it to disk. -fn resize_and_save_png(source: &DynamicImage, size: u32, file_path: &Path) -> Result<()> { - let image = source.resize_exact(size, size, FilterType::Lanczos3); +fn resize_and_save_png( + source: &Source, + size: u32, + file_path: &Path, + bg_color: Option>, +) -> Result<()> { + let mut image = source.resize_exact(size)?; + + if let Some(bg_color) = bg_color { + let mut bg_img = ImageBuffer::from_fn(size, size, |_, _| bg_color); + image::imageops::overlay(&mut bg_img, &image, 0, 0); + image = bg_img.into(); + } + let mut out_file = BufWriter::new(File::create(file_path)?); write_png(image.as_bytes(), &mut out_file, size)?; Ok(out_file.flush()?)