[feat (issue #6389)] make tauri icon support SVGs (#6444)

Co-authored-by: Fetzer <fetz@fetzverse.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Co-authored-by: FabianLars <fabianlars@fabianlars.de>
This commit is contained in:
fetzsav 2023-11-07 07:58:01 -05:00 committed by GitHub
parent fd532da899
commit 50f7ccbbf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 409 additions and 25 deletions

6
.changes/icon-svg.md Normal file
View File

@ -0,0 +1,6 @@
---
"tauri-cli": patch:feat
"@tauri-apps/cli": patch:feat
---
Add suport to SVG input image for the `tauri icon` command.

307
tooling/cli/Cargo.lock generated
View File

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

View File

@ -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" ] }

View File

@ -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<DynamicImage> {
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::<Vec<PngEntry>>()
{
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<String, IcnsEntry> =
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<u8>) -> Result<()> {
fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
fn desktop_entries(out_dir: &Path) -> Vec<PngEntry> {
let mut entries = Vec::new();
@ -383,27 +448,32 @@ fn png(source: &DynamicImage, out_dir: &Path, ios_color: Rgba<u8>) -> 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<Rgba<u8>>,
) -> 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()?)