소스 검색

feat(cli): Add `icon` command (tauricon) (#4992)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Fabian-Lars 2 년 전
부모
커밋
12e9d811e6
7개의 변경된 파일269개의 추가작업 그리고 48개의 파일을 삭제
  1. 5 0
      .changes/cli-tauricon.md
  2. 1 1
      tooling/bundler/Cargo.toml
  3. 18 47
      tooling/cli/Cargo.lock
  4. 2 0
      tooling/cli/Cargo.toml
  5. 52 0
      tooling/cli/src/helpers/icns.json
  6. 188 0
      tooling/cli/src/icon.rs
  7. 3 0
      tooling/cli/src/lib.rs

+ 5 - 0
.changes/cli-tauricon.md

@@ -0,0 +1,5 @@
+---
+"cli.rs": minor
+---
+
+Add `icon` command to generate icons.

+ 1 - 1
tooling/bundler/Cargo.toml

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

+ 18 - 47
tooling/cli/Cargo.lock

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

+ 2 - 0
tooling/cli/Cargo.toml

@@ -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 - 0
tooling/cli/src/helpers/icns.json

@@ -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 - 0
tooling/cli/src/icon.rs

@@ -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(())
+}

+ 3 - 0
tooling/cli/src/lib.rs

@@ -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)?,