diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8b4ad89 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,70 @@ +use std::fmt::Debug; +use std::num::ParseIntError; + +use clap::Parser; +use image::{DynamicImage, ImageReader, Rgb}; + +use crate::Colour; + +#[derive(Parser)] +#[command(name = "imagePicker")] +#[command(version = "1.0")] +#[command(about = "Matches colours to their closest equivalents in a given image", long_about = None)] +/// Matches the colours in one image to either a list of provided colours or a different image. +/// +/// For each provided colour (either explicitly or in the second image), finds the closest colour +/// from the first image, using redmean distance. +pub struct Cli { + // The source image from which colours should be picked. + #[arg(value_parser = valid_image_file)] + pub image: DynamicImage, + + // Optional second image; if used, all colours from this image will be matched to their closest + // equivalent in the first image. + #[arg(short = 'i', long = "image", value_parser = valid_image_file)] + pub second_image: Option, + + // Optional list of hex values; if used, will match each value to their closest equivalent in + // the first image + // + // Should provide valid hex codes only: '24274a', and '#cad3f5' are both valid. + #[arg(short = 'c', long = "colours", value_parser = valid_hex_code)] + pub colours: Option>, +} + +impl Debug for Cli { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cli") + .field("image", &self.image) + .field("image_two", &self.second_image) + .field("colours", &self.colours) + .finish() + } +} + +fn valid_hex_code(s: &str) -> Result { + // accept leading '#' for hex values + let start = match s.chars().nth(0) == Some('#') { + true => 1, + false => 0, + }; + + let u8s: Vec = (start..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect::, ParseIntError>>() + .map_err(|e| format!("{}", e))?; + let rgb: [u8; 3] = u8s + .try_into() + .map_err(|_| <&str as Into>::into("Hex string must decode to exactly 3 bytes"))?; + Ok(Rgb(rgb)) +} + +fn valid_image_file(s: &str) -> Result { + ImageReader::open(s) + .map_err(|e| format!("{}", e))? + .with_guessed_format() + .map_err(|e| format!("{}", e))? + .decode() + .map_err(|e| format!("{}", e)) +} diff --git a/src/main.rs b/src/main.rs index b56a430..f672dcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,15 @@ +mod cli; + +use clap::Parser; use std::{ collections::{HashMap, HashSet}, - env, num::ParseIntError, }; use dotenv::dotenv; -use image::{DynamicImage, GenericImageView, ImageReader, Rgb}; +use image::{DynamicImage, GenericImageView, Rgb}; + +use crate::cli::Cli; // TODO (medium priority): replace hashset with another hashmap>; // that way we can track pixel locations for colours. @@ -45,7 +49,7 @@ fn extract_colours_set(img: DynamicImage) -> HashSet> { out.into_iter().map(|c| Rgb([c[0], c[1], c[2]])).collect() } -fn closest_colour(colour: Colour, all: &ColourSet) -> Option { +fn closest_colour(colour: &Colour, all: &ColourSet) -> Option { // use redmean to calculate the distance between colours // inevitably O(len(all)) let mut closest: Option = None; @@ -71,31 +75,23 @@ fn closest_colour(colour: Colour, all: &ColourSet) -> Option { return closest; } -fn map_image_list(img_file: &str, args: &Vec) -> HashMap { - let img = ImageReader::open(img_file).unwrap().decode().unwrap(); // FIXME: unwraps - - let all_cols = extract_colours(img); +fn map_image_list(all_cols: &ColourSet, cols: Vec) -> HashMap { let mut output_cols: HashMap = HashMap::new(); - args.iter().skip(2).for_each(|c_str| { - let c = decode_hex(c_str).unwrap(); // FIXME: unwraps - if let Some(col) = closest_colour(c, &all_cols) { + for c in cols { + if let Some(col) = closest_colour(&c, &all_cols) { output_cols.insert(c, col); } - }); + } output_cols } -fn map_image_image(img_file_1: &str, img_file_2: &str) -> HashMap { - let img_1 = ImageReader::open(img_file_1).unwrap().decode().unwrap(); - let img_2 = ImageReader::open(img_file_2).unwrap().decode().unwrap(); - - let img_1_cols = extract_colours_set(img_1); - let img_2_cols = extract_colours(img_2); +fn map_image_image(img_1_cols: &ColourSet, img_2: DynamicImage) -> HashMap { + let img_2_cols = extract_colours_set(img_2); let mut output_cols: HashMap = HashMap::new(); - img_1_cols.iter().for_each(|c| { - if let Some(col) = closest_colour(*c, &img_2_cols) { + img_2_cols.iter().for_each(|c| { + if let Some(col) = closest_colour(c, &img_1_cols) { output_cols.insert(*c, col); } }); @@ -103,22 +99,29 @@ fn map_image_image(img_file_1: &str, img_file_2: &str) -> HashMap String { + format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2]) +} + +fn pretty_print(h: HashMap) { + h.iter() + .for_each(|(k, v)| println!("{}: {}", encode_hex(k), encode_hex(v))) +} + fn main() { dotenv().ok(); pretty_env_logger::init(); + let cli = Cli::parse(); - // TODO (high priority): proper arg parsing - let args: Vec = env::args().collect(); - if args.len() > 2 { - // list of colours and image - // let out_cols = map_image_list(&args[1], &args); - let out_cols = map_image_image(&args[1], &args[2]); + let set = extract_colours(cli.image); - out_cols.iter().for_each(|(k, v)| { - println!( - "({}, {}, {}): ({}, {}, {})", - k[0], k[1], k[2], v[0], v[1], v[2] - ) - }) + if let Some(cs) = cli.colours { + let colours = map_image_list(&set, cs); + pretty_print(colours); + } + + if let Some(i) = cli.second_image { + let colours = map_image_image(&set, i); + pretty_print(colours); } }