mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-02-06 13:57:16 +00:00
feat(cli): Add icon command (tauricon) (#4992)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
parent
0837454b00
commit
12e9d811e6
5
.changes/cli-tauricon.md
Normal file
5
.changes/cli-tauricon.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"cli.rs": minor
|
||||
---
|
||||
|
||||
Add `icon` command to generate icons.
|
||||
@ -49,7 +49,7 @@ zip = "0.6"
|
||||
semver = "1"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
icns = "0.3"
|
||||
icns = { package = "tauri-icns", version = "0.1" }
|
||||
time = { version = "0.3", features = [ "formatting" ] }
|
||||
plist = "1"
|
||||
|
||||
|
||||
65
tooling/cli/Cargo.lock
generated
65
tooling/cli/Cargo.lock
generated
@ -590,16 +590,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "1.0.0"
|
||||
@ -748,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"deflate 1.0.0",
|
||||
"deflate",
|
||||
"flume",
|
||||
"half",
|
||||
"inflate",
|
||||
@ -791,7 +781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.5.3",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1073,16 +1063,6 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "icns"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ccfbad7e08da70a5b48a924994a5afd93125ce5d45a3b0ba0b8da7bda59a40"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png 0.16.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@ -1132,7 +1112,7 @@ dependencies = [
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"png 0.17.5",
|
||||
"png",
|
||||
"scoped_threadpool",
|
||||
"tiff",
|
||||
]
|
||||
@ -1484,15 +1464,6 @@ dependencies = [
|
||||
"scrypt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.3"
|
||||
@ -2051,18 +2022,6 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.16.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"deflate 0.8.6",
|
||||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.5"
|
||||
@ -2071,8 +2030,8 @@ checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"deflate 1.0.0",
|
||||
"miniz_oxide 0.5.3",
|
||||
"deflate",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2828,7 +2787,6 @@ dependencies = [
|
||||
"handlebars",
|
||||
"heck",
|
||||
"hex",
|
||||
"icns",
|
||||
"image",
|
||||
"libflate",
|
||||
"log",
|
||||
@ -2841,6 +2799,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"strsim",
|
||||
"tar",
|
||||
"tauri-icns",
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
@ -2867,6 +2826,7 @@ dependencies = [
|
||||
"handlebars",
|
||||
"heck",
|
||||
"ignore",
|
||||
"image",
|
||||
"include_dir",
|
||||
"json-patch",
|
||||
"lazy_static",
|
||||
@ -2885,6 +2845,7 @@ dependencies = [
|
||||
"serde_with 2.0.0",
|
||||
"shared_child",
|
||||
"tauri-bundler",
|
||||
"tauri-icns",
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"terminal_size 0.2.1",
|
||||
@ -2909,6 +2870,16 @@ dependencies = [
|
||||
"tauri-cli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-icns"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.0.3"
|
||||
|
||||
@ -65,6 +65,8 @@ ignore = "0.4"
|
||||
ctrlc = "3.2"
|
||||
log = { version = "0.4.17", features = [ "kv_unstable", "kv_unstable_std" ] }
|
||||
env_logger = "0.9.0"
|
||||
icns = { package = "tauri-icns", version = "0.1" }
|
||||
image = { version = "0.24", default-features = false, features = [ "ico" ] }
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
winapi = { version = "0.3", features = [ "handleapi", "processenv", "winbase", "wincon", "winnt" ] }
|
||||
|
||||
52
tooling/cli/src/helpers/icns.json
Normal file
52
tooling/cli/src/helpers/icns.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"16x16": {
|
||||
"name": "icon_16x16.png",
|
||||
"size": 16,
|
||||
"ostype": "icp4"
|
||||
},
|
||||
"16x16@2x": {
|
||||
"name": "icon_16x16@2x.png",
|
||||
"size": 32,
|
||||
"ostype": "ic11"
|
||||
},
|
||||
"32x32": {
|
||||
"name": "icon_32x32.png",
|
||||
"size": 32,
|
||||
"ostype": "icp5"
|
||||
},
|
||||
"32x32@2x": {
|
||||
"name": "icon_32x32@2x.png",
|
||||
"size": 64,
|
||||
"ostype": "ic12"
|
||||
},
|
||||
"128x128": {
|
||||
"name": "icon_128x128.png",
|
||||
"size": 128,
|
||||
"ostype": "ic07"
|
||||
},
|
||||
"128x128@2x": {
|
||||
"name": "icon_128x128@2x.png",
|
||||
"size": 256,
|
||||
"ostype": "ic13"
|
||||
},
|
||||
"256x256": {
|
||||
"name": "icon_256x256.png",
|
||||
"size": 256,
|
||||
"ostype": "ic08"
|
||||
},
|
||||
"256x256@2x": {
|
||||
"name": "icon_256x256@2x.png",
|
||||
"size": 512,
|
||||
"ostype": "ic14"
|
||||
},
|
||||
"512x512": {
|
||||
"name": "icon_512x512.png",
|
||||
"size": 512,
|
||||
"ostype": "ic09"
|
||||
},
|
||||
"512x512@2x": {
|
||||
"name": "icon_512x512@2x.png",
|
||||
"size": 1024,
|
||||
"ostype": "ic10"
|
||||
}
|
||||
}
|
||||
188
tooling/cli/src/icon.rs
Normal file
188
tooling/cli/src/icon.rs
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{helpers::app_paths::tauri_dir, Result};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{create_dir_all, File},
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use icns::{IconFamily, IconType};
|
||||
use image::{
|
||||
codecs::{
|
||||
ico::{IcoEncoder, IcoFrame},
|
||||
png::{CompressionType, FilterType as PngFilterType, PngEncoder},
|
||||
},
|
||||
imageops::FilterType,
|
||||
open, ColorType, DynamicImage, ImageEncoder,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IcnsEntry {
|
||||
size: u32,
|
||||
ostype: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Generates various icons for all major platforms")]
|
||||
pub struct Options {
|
||||
// TODO: Confirm 1240px
|
||||
/// Path to the source icon (png, 1240x1240px with transparency).
|
||||
#[clap(default_value = "./app-icon.png")]
|
||||
input: PathBuf,
|
||||
/// Output directory.
|
||||
/// Default: 'icons' directory next to the tauri.conf.json file.
|
||||
#[clap(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let input = options.input;
|
||||
let out_dir = options.output.unwrap_or_else(|| tauri_dir().join("icons"));
|
||||
create_dir_all(&out_dir).context("Can't create output directory")?;
|
||||
|
||||
// Try to read the image as a DynamicImage, convert it to rgba8 and turn it into a DynamicImage again.
|
||||
// Both things should be catched by the explicit conversions to rgba8 anyway.
|
||||
let source = open(input)
|
||||
.context("Can't read and decode source image")?
|
||||
.into_rgba8();
|
||||
|
||||
let source = DynamicImage::ImageRgba8(source);
|
||||
|
||||
if source.height() != source.width() {
|
||||
panic!("Source image must be square");
|
||||
}
|
||||
|
||||
appx(&source, &out_dir).context("Failed to generate appx icons")?;
|
||||
|
||||
icns(&source, &out_dir).context("Failed to generate .icns file")?;
|
||||
|
||||
ico(&source, &out_dir).context("Failed to generate .ico file")?;
|
||||
|
||||
png(&source, &out_dir).context("Failed to generate png icons")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn appx(source: &DynamicImage, out_dir: &Path) -> Result<()> {
|
||||
log::info!(action = "Appx"; "Creating StoreLogo.png");
|
||||
resize_and_save_png(source, 50, &out_dir.join("StoreLogo.png"))?;
|
||||
|
||||
for size in [30, 44, 71, 89, 107, 142, 150, 284, 310] {
|
||||
let file_name = format!("Square{}x{}Logo.png", size, size);
|
||||
log::info!(action = "Appx"; "Creating {}", file_name);
|
||||
|
||||
resize_and_save_png(source, size, &out_dir.join(&file_name))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Main target: macOS
|
||||
fn icns(source: &DynamicImage, 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();
|
||||
|
||||
let mut family = IconFamily::new();
|
||||
|
||||
for (name, entry) in entries {
|
||||
let size = entry.size;
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let image = source.resize_exact(size, size, FilterType::Lanczos3);
|
||||
|
||||
write_png(image.as_bytes(), &mut buf, size)?;
|
||||
|
||||
let image = icns::Image::read_png(&buf[..])?;
|
||||
|
||||
family
|
||||
.add_icon_with_type(
|
||||
&image,
|
||||
IconType::from_ostype(entry.ostype.parse().unwrap()).unwrap(),
|
||||
)
|
||||
.with_context(|| format!("Can't add {} to Icns Family", name))?;
|
||||
}
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(out_dir.join("icon.icns"))?);
|
||||
family.write(&mut out_file)?;
|
||||
out_file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Generate .ico file with layers for the most common sizes.
|
||||
// Main target: Windows
|
||||
fn ico(source: &DynamicImage, 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);
|
||||
|
||||
// Only the 256px layer can be compressed according to the ico specs.
|
||||
if size == 256 {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
write_png(image.as_bytes(), &mut buf, size)?;
|
||||
|
||||
frames.push(IcoFrame::with_encoded(buf, size, size, ColorType::Rgba8)?)
|
||||
} else {
|
||||
frames.push(IcoFrame::as_png(
|
||||
image.as_bytes(),
|
||||
size,
|
||||
size,
|
||||
ColorType::Rgba8,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(out_dir.join("icon.ico"))?);
|
||||
let encoder = IcoEncoder::new(&mut out_file);
|
||||
encoder.encode_images(&frames)?;
|
||||
out_file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Generate .png files in 32x32, 128x128, 256x256, 512x512 (icon.png)
|
||||
// Main target: Linux
|
||||
fn png(source: &DynamicImage, out_dir: &Path) -> Result<()> {
|
||||
for size in [32, 128, 256, 512] {
|
||||
let file_name = match size {
|
||||
256 => "128x128@2.png".to_string(),
|
||||
512 => "icon.png".to_string(),
|
||||
_ => format!("{}x{}.png", size, size),
|
||||
};
|
||||
|
||||
log::info!(action = "PNG"; "Creating {}", file_name);
|
||||
resize_and_save_png(source, size, &out_dir.join(&file_name))?;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(file_path)?);
|
||||
|
||||
write_png(image.as_bytes(), &mut out_file, size)?;
|
||||
|
||||
Ok(out_file.flush()?)
|
||||
}
|
||||
|
||||
// Encode image data as png with compression.
|
||||
fn write_png<W: Write>(image_data: &[u8], w: W, size: u32) -> Result<()> {
|
||||
let encoder = PngEncoder::new_with_quality(w, CompressionType::Best, PngFilterType::Adaptive);
|
||||
encoder.write_image(image_data, size, size, ColorType::Rgba8)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -7,6 +7,7 @@ pub use anyhow::Result;
|
||||
mod build;
|
||||
mod dev;
|
||||
mod helpers;
|
||||
mod icon;
|
||||
mod info;
|
||||
mod init;
|
||||
mod interface;
|
||||
@ -62,6 +63,7 @@ struct Cli {
|
||||
enum Commands {
|
||||
Build(build::Options),
|
||||
Dev(dev::Options),
|
||||
Icon(icon::Options),
|
||||
Info(info::Options),
|
||||
Init(init::Options),
|
||||
Plugin(plugin::Cli),
|
||||
@ -160,6 +162,7 @@ where
|
||||
match cli.command {
|
||||
Commands::Build(options) => build::command(options)?,
|
||||
Commands::Dev(options) => dev::command(options)?,
|
||||
Commands::Icon(options) => icon::command(options)?,
|
||||
Commands::Info(options) => info::command(options)?,
|
||||
Commands::Init(options) => init::command(options)?,
|
||||
Commands::Plugin(cli) => plugin::command(cli)?,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user