use clap::{Args, Parser};
use std::path::PathBuf;
pub const DEFAULT_SIZE: u32 = 1024;
pub const DEFAULT_SUFFIX: &str = "svgy";
pub const ROUND_SUFFIX: &str = "round";
pub const DEFAULT_ROUND_PADDING: f64 = 0.1;
pub const DEFAULT_PRECISION_THRESHOLD: f64 = 0.02;
#[derive(Parser)]
#[command(
name = "svgy",
version,
about = "Convert an SVG to PNG / ICO / ICNS, or resize an SVG"
)]
#[allow(clippy::option_option)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
pub input: PathBuf,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
pub svg: Option<Option<PathBuf>>,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
pub png: Option<Option<PathBuf>>,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
pub ico: Option<Option<PathBuf>>,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
pub icns: Option<Option<PathBuf>>,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
pub round: Option<Option<PathBuf>>,
#[command(flatten)]
pub size: SizeOpts,
#[arg(
long,
value_name = "FRACTION",
num_args = 0..=1,
require_equals = true,
default_missing_value = "0.1"
)]
pub padding: Option<f64>,
#[arg(
long,
value_name = "FRACTION",
num_args = 0..=1,
require_equals = true,
default_missing_value = "0.02"
)]
pub precision: Option<f64>,
#[arg(long = "no-precision")]
pub no_precision: bool,
#[arg(long, default_value = DEFAULT_SUFFIX)]
pub suffix: String,
#[arg(long = "no-resize")]
pub no_resize: bool,
#[arg(long = "no-square")]
pub no_square: bool,
#[arg(long = "no-optimize")]
pub no_optimize: bool,
#[arg(long)]
pub zopfli: bool,
#[arg(long = "no-legacy-ico")]
pub no_legacy: bool,
#[arg(long = "strip-images")]
pub strip_images: bool,
}
impl Cli {
pub fn no_target(&self) -> bool {
self.svg.is_none()
&& self.png.is_none()
&& self.ico.is_none()
&& self.icns.is_none()
&& self.round.is_none()
}
pub fn svg_target(&self) -> Option<&Option<PathBuf>> {
if self.no_target() {
return Some(&None);
}
self.svg.as_ref()
}
}
#[derive(Clone, Copy)]
pub enum Target {
Square(u32),
Width(u32),
Height(u32),
Both(u32, u32),
}
impl Target {
pub fn canvas(self) -> (Option<f64>, Option<f64>) {
match self {
Target::Square(n) => (Some(f64::from(n)), Some(f64::from(n))),
Target::Width(w) => (Some(f64::from(w)), None),
Target::Height(h) => (None, Some(f64::from(h))),
Target::Both(w, h) => (Some(f64::from(w)), Some(f64::from(h))),
}
}
}
#[derive(Args, Clone, Copy)]
pub struct SizeOpts {
#[arg(short = 's', long = "size")]
pub size: Option<u32>,
#[arg(long)]
pub width: Option<u32>,
#[arg(long)]
pub height: Option<u32>,
}
impl SizeOpts {
pub fn target(&self) -> Option<Target> {
match (self.size, self.width, self.height) {
(Some(s), _, _) => Some(Target::Square(s)),
(None, Some(w), Some(h)) => Some(Target::Both(w, h)),
(None, Some(w), None) => Some(Target::Width(w)),
(None, None, Some(h)) => Some(Target::Height(h)),
(None, None, None) => None,
}
}
pub fn target_or_default(&self) -> Target {
self.target().unwrap_or(Target::Square(DEFAULT_SIZE))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use std::path::Path;
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("parse")
}
#[test]
fn bare_input_implies_svg() {
let cli = parse(&["svgy", "logo.svg"]);
assert!(cli.no_target());
assert!(matches!(cli.svg_target(), Some(None)));
}
#[test]
fn explicit_target_drops_the_implicit_svg() {
let cli = parse(&["svgy", "logo.svg", "--png"]);
assert!(!cli.no_target());
assert!(cli.svg_target().is_none());
assert!(matches!(cli.png, Some(None)));
}
#[test]
fn svg_can_be_kept_alongside_another_target() {
let cli = parse(&["svgy", "logo.svg", "--png", "--svg"]);
assert!(matches!(cli.svg_target(), Some(None)));
assert!(matches!(cli.png, Some(None)));
}
#[test]
fn targets_take_an_optional_path() {
let cli = parse(&["svgy", "logo.svg", "--icns=macos/logo.icns", "--ico"]);
assert_eq!(
cli.icns.as_ref().unwrap().as_deref(),
Some(Path::new("macos/logo.icns"))
);
assert!(matches!(cli.ico, Some(None)));
}
#[test]
fn bare_padding_is_the_default_fraction() {
let cli = parse(&["svgy", "logo.svg", "--round", "--padding"]);
assert_eq!(cli.padding, Some(DEFAULT_ROUND_PADDING));
let cli = parse(&["svgy", "logo.svg", "--round", "--padding=0.25"]);
assert_eq!(cli.padding, Some(0.25));
let cli = parse(&["svgy", "logo.svg", "--round"]);
assert_eq!(cli.padding, None);
}
#[test]
fn bare_precision_is_the_default_threshold() {
let cli = parse(&["svgy", "logo.svg", "--precision"]);
assert_eq!(cli.precision, Some(DEFAULT_PRECISION_THRESHOLD));
let cli = parse(&["svgy", "logo.svg", "--precision=0.005"]);
assert_eq!(cli.precision, Some(0.005));
let cli = parse(&["svgy", "logo.svg"]);
assert_eq!(cli.precision, None);
}
#[test]
fn the_search_runs_unless_no_precision_is_given() {
use crate::svg_optimize::{Opts, Precision};
let cli = parse(&["svgy", "logo.svg"]);
assert!(matches!(
Opts::from_cli(&cli).precision,
Precision::Adaptive { threshold } if threshold == DEFAULT_PRECISION_THRESHOLD
));
let cli = parse(&["svgy", "logo.svg", "--no-precision"]);
assert!(matches!(Opts::from_cli(&cli).precision, Precision::Default));
let cli = parse(&["svgy", "logo.svg", "--no-precision", "--precision=0.5"]);
assert!(matches!(Opts::from_cli(&cli).precision, Precision::Default));
}
#[test]
fn zopfli_is_off_by_default() {
let cli = parse(&["svgy", "logo.svg", "--png"]);
assert!(!cli.zopfli);
let cli = parse(&["svgy", "logo.svg", "--png", "--zopfli"]);
assert!(cli.zopfli);
}
#[test]
fn size_defaults_to_the_documented_value() {
let cli = parse(&["svgy", "logo.svg"]);
assert!(matches!(
cli.size.target_or_default(),
Target::Square(DEFAULT_SIZE)
));
let cli = parse(&["svgy", "logo.svg", "--width=200"]);
assert!(matches!(cli.size.target_or_default(), Target::Width(200)));
}
}