diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 195 |
1 files changed, 105 insertions, 90 deletions
diff --git a/src/main.rs b/src/main.rs index e9df476..204a582 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,8 @@ use crate::frontier::mean::MeanFrontier; use crate::frontier::min::MinFrontier; use crate::frontier::Frontier; -use clap::{self, clap_app, crate_authors, crate_name, crate_version}; +use clap::{ArgAction, CommandFactory, Parser, ValueEnum}; +use clap::error::ErrorKind; use image::{self, ColorType, ImageEncoder, ImageError, Rgba, RgbaImage}; use image::codecs::png::{CompressionType, FilterType, PngEncoder}; @@ -24,7 +25,6 @@ use std::error::Error; use std::io::{self, BufWriter, IsTerminal, Write}; use std::path::PathBuf; use std::process::exit; -use std::str::FromStr; use std::time::Instant; /// The color source specified on the command line. @@ -50,29 +50,107 @@ enum OrderArg { } /// The frontier implementation. -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)] enum FrontierArg { /// Pick a neighbor of the closest pixel so far. Min, /// Pick the pixel with the closest mean color of all its neighbors. Mean, /// Target the closest pixel on an image. + #[value(skip)] Image(PathBuf), } /// The color space to operate in. -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] enum ColorSpaceArg { /// sRGB space. + #[value(name = "RGB")] Rgb, /// CIE L*a*b* space. + #[value(name = "Lab")] Lab, /// CIE L*u*v* space. + #[value(name = "Luv")] Luv, /// Oklab space. + #[value(name = "Oklab")] Oklab, } +/// k-d forests. +#[derive(Debug, Parser)] +#[command(author, version, about, disable_help_flag = true)] +struct Cli { + /// Use all <DEPTH>-bit colors. + #[arg(short, long, group = "source", value_name = "DEPTH", default_value = "24")] + bit_depth: Option<String>, + /// use colors from the <INPUT> image. + #[arg(short, long, group = "source", value_name = "INPUT")] + input: Option<PathBuf>, + + /// Sort colors by hue [default]. + #[arg(short = 's', long, group = "order", default_value_t = true)] + hue_sort: bool, + /// Randomize colors. + #[arg(short, long, group = "order")] + random: bool, + /// Place colors in Morton order (Z-order). + #[arg(short = 'M', long, group = "order")] + morton: bool, + /// Place colors in Hilbert curve order + #[arg(short = 'H', long, group = "order")] + hilbert: bool, + + /// Reduce artifacts by iterating through the colors in multiple stripes [default]. + #[arg(short = 't', long, group = "stripe?", default_value_t = true)] + stripe: bool, + /// Don't stripe. + #[arg(short = 'T', long, group = "stripe?")] + no_stripe: bool, + + /// Specify the selection mode. + #[arg(short = 'l', long, group = "frontier", value_name = "MODE", default_value = "min")] + selection: FrontierArg, + /// Place colors on the closest pixels of the <TARGET> image. + #[arg(short = 'g', long, group = "frontier", value_name = "TARGET")] + target: Option<PathBuf>, + + /// Use the given color space. + #[arg(short, long, value_name = "SPACE", default_value = "Lab")] + color_space: ColorSpaceArg, + + /// The width of the generated image. + #[arg(short, long)] + width: Option<u32>, + /// The height of the generated image. + #[arg(short, long)] + height: Option<u32>, + + /// The x coordinate of the first pixel. + #[arg(short, value_name = "X")] + x0: Option<u32>, + /// The y coordinate of the first pixel. + #[arg(short, value_name = "Y")] + y0: Option<u32>, + + /// Generate frames of an animation. + #[arg(short, long)] + animate: bool, + + /// Save the image to <PATH>. + #[arg(short, long, value_name = "PATH", default_value = "kd-forest.png")] + output: PathBuf, + + /// Seed the random number generator. + #[arg(short = 'e', long, default_value_t = 0)] + seed: u64, + + /// Print help. + #[arg(short = '?', long, action = ArgAction::Help)] + help: (), +} + /// Error type for this app. #[derive(Debug)] enum AppError { @@ -83,10 +161,7 @@ enum AppError { impl AppError { /// Create an error for an invalid argument. fn invalid_value(msg: &str) -> Self { - Self::ArgError(clap::Error::with_description( - msg, - clap::ErrorKind::InvalidValue, - )) + Self::ArgError(Cli::command().error(ErrorKind::InvalidValue, msg)) } /// Exit the program with this error. @@ -128,19 +203,6 @@ impl From<rand::Error> for AppError { /// Result type for this app. type AppResult<T> = Result<T, AppError>; -/// Parse an argument into the appropriate type. -fn parse_arg<F>(arg: Option<&str>) -> AppResult<Option<F>> -where - F: FromStr, - F::Err: Error, -{ - match arg.map(|s| s.parse()) { - Some(Ok(f)) => Ok(Some(f)), - Some(Err(e)) => Err(AppError::invalid_value(&e.to_string())), - None => Ok(None), - } -} - /// The parsed command line arguments. #[derive(Debug)] struct Args { @@ -160,49 +222,14 @@ struct Args { impl Args { fn parse() -> AppResult<Self> { - let args = clap_app!((crate_name!()) => - (version: crate_version!()) - (author: crate_authors!()) - (@setting ColoredHelp) - (@setting DeriveDisplayOrder) - (@setting UnifiedHelpMessage) - (@group source => - (@arg DEPTH: -b --("bit-depth") +takes_value "Use all DEPTH-bit colors") - (@arg INPUT: -i --input +takes_value "Use colors from the INPUT image") - ) - (@group order => - (@arg HUE: -s --hue-sort "Sort colors by hue [default]") - (@arg RANDOM: -r --random "Randomize colors") - (@arg MORTON: -M --morton "Place colors in Morton order (Z-order)") - (@arg HILBERT: -H --hilbert "Place colors in Hilbert curve order") - ) - (@group stripe => - (@arg STRIPE: -t --stripe "Reduce artifacts by iterating through the colors in multiple stripes [default]") - (@arg NOSTRIPE: -T --("no-stripe") "Don't stripe") - ) - (@group frontier => - (@arg MODE: -l --selection +takes_value possible_value[min mean] "Specify the selection mode") - (@arg TARGET: -g --target +takes_value "Place colors on the closest pixels of the TARGET image") - ) - (@arg SPACE: -c --("color-space") default_value("Lab") possible_value[RGB Lab Luv Oklab] "Use the given color space") - (@arg WIDTH: -w --width +takes_value "The width of the generated image") - (@arg HEIGHT: -h --height +takes_value "The height of the generated image") - (@arg X: -x +takes_value "The x coordinate of the first pixel") - (@arg Y: -y +takes_value "The y coordinate of the first pixel") - (@arg ANIMATE: -a --animate "Generate frames of an animation") - (@arg PATH: -o --output default_value("kd-forest.png") "Save the image to PATH") - (@arg SEED: -e --seed default_value("0") "Seed the random number generator") - ) - .get_matches_safe()?; + let args = Cli::try_parse()?; - let source = if let Some(input) = args.value_of("INPUT") { - SourceArg::Image(PathBuf::from(input)) + let source = if let Some(input) = args.input { + SourceArg::Image(input) } else { - let arg = args.value_of("DEPTH"); + let arg = args.bit_depth.unwrap(); let depths: Vec<_> = arg - .iter() - .map(|s| s.split(',')) - .flatten() + .split(',') .map(|n| n.parse().ok()) .collect(); @@ -216,62 +243,50 @@ impl Args { _ => { return Err(AppError::invalid_value( - &format!("invalid bit depth {}", arg.unwrap()), + &format!("invalid bit depth {}", arg), )); } }; if r > 8 || g > 8 || b > 8 { return Err(AppError::invalid_value( - &format!("bit depth of {} is too deep!", arg.unwrap()), + &format!("bit depth of {} is too deep!", arg), )); } SourceArg::AllRgb(r, g, b) }; - let order = if args.is_present("RANDOM") { + let order = if args.random { OrderArg::Random - } else if args.is_present("MORTON") { + } else if args.morton { OrderArg::Morton - } else if args.is_present("HILBERT") { + } else if args.hilbert { OrderArg::Hilbert } else { OrderArg::HueSort }; - let stripe = !args.is_present("NOSTRIPE") && order != OrderArg::Random; + let stripe = !args.no_stripe && order != OrderArg::Random; - let frontier = if let Some(target) = args.value_of("TARGET") { - FrontierArg::Image(PathBuf::from(target)) + let frontier = if let Some(target) = args.target { + FrontierArg::Image(target) } else { - match args.value_of("MODE") { - Some("min") | None => FrontierArg::Min, - Some("mean") => FrontierArg::Mean, - _ => unreachable!(), - } + args.selection }; - let space = match args.value_of("SPACE").unwrap() { - "RGB" => ColorSpaceArg::Rgb, - "Lab" => ColorSpaceArg::Lab, - "Luv" => ColorSpaceArg::Luv, - "Oklab" => ColorSpaceArg::Oklab, - _ => unreachable!(), - }; + let space = args.color_space; - let width = parse_arg(args.value_of("WIDTH"))?; - let height = parse_arg(args.value_of("HEIGHT"))?; - let x0 = parse_arg(args.value_of("X"))?; - let y0 = parse_arg(args.value_of("Y"))?; + let width = args.width; + let height = args.height; + let x0 = args.x0; + let y0 = args.y0; - let animate = args.is_present("ANIMATE"); + let animate = args.animate; - let output = args.value_of("PATH") - .map(PathBuf::from) - .unwrap(); + let output = args.output; - let seed = parse_arg(args.value_of("SEED"))?.unwrap_or(0); + let seed = args.seed; Ok(Self { source, |