ft: arg parsing
This commit is contained in:
70
src/cli.rs
Normal file
70
src/cli.rs
Normal file
@@ -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<DynamicImage>,
|
||||||
|
|
||||||
|
// 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<Vec<Colour>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Colour, String> {
|
||||||
|
// accept leading '#' for hex values
|
||||||
|
let start = match s.chars().nth(0) == Some('#') {
|
||||||
|
true => 1,
|
||||||
|
false => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let u8s: Vec<u8> = (start..s.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
|
||||||
|
.collect::<Result<Vec<u8>, ParseIntError>>()
|
||||||
|
.map_err(|e| format!("{}", e))?;
|
||||||
|
let rgb: [u8; 3] = u8s
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| <&str as Into<String>>::into("Hex string must decode to exactly 3 bytes"))?;
|
||||||
|
Ok(Rgb(rgb))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_image_file(s: &str) -> Result<DynamicImage, String> {
|
||||||
|
ImageReader::open(s)
|
||||||
|
.map_err(|e| format!("{}", e))?
|
||||||
|
.with_guessed_format()
|
||||||
|
.map_err(|e| format!("{}", e))?
|
||||||
|
.decode()
|
||||||
|
.map_err(|e| format!("{}", e))
|
||||||
|
}
|
||||||
65
src/main.rs
65
src/main.rs
@@ -1,11 +1,15 @@
|
|||||||
|
mod cli;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
env,
|
|
||||||
num::ParseIntError,
|
num::ParseIntError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use dotenv::dotenv;
|
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<u8, Vec<(u32, u32)>>;
|
// TODO (medium priority): replace hashset with another hashmap<u8, Vec<(u32, u32)>>;
|
||||||
// that way we can track pixel locations for colours.
|
// that way we can track pixel locations for colours.
|
||||||
@@ -45,7 +49,7 @@ fn extract_colours_set(img: DynamicImage) -> HashSet<Rgb<u8>> {
|
|||||||
out.into_iter().map(|c| Rgb([c[0], c[1], c[2]])).collect()
|
out.into_iter().map(|c| Rgb([c[0], c[1], c[2]])).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn closest_colour(colour: Colour, all: &ColourSet) -> Option<Colour> {
|
fn closest_colour(colour: &Colour, all: &ColourSet) -> Option<Colour> {
|
||||||
// use redmean to calculate the distance between colours
|
// use redmean to calculate the distance between colours
|
||||||
// inevitably O(len(all))
|
// inevitably O(len(all))
|
||||||
let mut closest: Option<Colour> = None;
|
let mut closest: Option<Colour> = None;
|
||||||
@@ -71,31 +75,23 @@ fn closest_colour(colour: Colour, all: &ColourSet) -> Option<Colour> {
|
|||||||
return closest;
|
return closest;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_image_list(img_file: &str, args: &Vec<String>) -> HashMap<Colour, Colour> {
|
fn map_image_list(all_cols: &ColourSet, cols: Vec<Colour>) -> HashMap<Colour, Colour> {
|
||||||
let img = ImageReader::open(img_file).unwrap().decode().unwrap(); // FIXME: unwraps
|
|
||||||
|
|
||||||
let all_cols = extract_colours(img);
|
|
||||||
let mut output_cols: HashMap<Colour, Colour> = HashMap::new();
|
let mut output_cols: HashMap<Colour, Colour> = HashMap::new();
|
||||||
|
|
||||||
args.iter().skip(2).for_each(|c_str| {
|
for c in cols {
|
||||||
let c = decode_hex(c_str).unwrap(); // FIXME: unwraps
|
if let Some(col) = closest_colour(&c, &all_cols) {
|
||||||
if let Some(col) = closest_colour(c, &all_cols) {
|
|
||||||
output_cols.insert(c, col);
|
output_cols.insert(c, col);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
output_cols
|
output_cols
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_image_image(img_file_1: &str, img_file_2: &str) -> HashMap<Colour, Colour> {
|
fn map_image_image(img_1_cols: &ColourSet, img_2: DynamicImage) -> HashMap<Colour, Colour> {
|
||||||
let img_1 = ImageReader::open(img_file_1).unwrap().decode().unwrap();
|
let img_2_cols = extract_colours_set(img_2);
|
||||||
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);
|
|
||||||
|
|
||||||
let mut output_cols: HashMap<Colour, Colour> = HashMap::new();
|
let mut output_cols: HashMap<Colour, Colour> = HashMap::new();
|
||||||
img_1_cols.iter().for_each(|c| {
|
img_2_cols.iter().for_each(|c| {
|
||||||
if let Some(col) = closest_colour(*c, &img_2_cols) {
|
if let Some(col) = closest_colour(c, &img_1_cols) {
|
||||||
output_cols.insert(*c, col);
|
output_cols.insert(*c, col);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -103,22 +99,29 @@ fn map_image_image(img_file_1: &str, img_file_2: &str) -> HashMap<Colour, Colour
|
|||||||
output_cols
|
output_cols
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_hex(c: &Colour) -> String {
|
||||||
|
format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pretty_print(h: HashMap<Colour, Colour>) {
|
||||||
|
h.iter()
|
||||||
|
.for_each(|(k, v)| println!("{}: {}", encode_hex(k), encode_hex(v)))
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// TODO (high priority): proper arg parsing
|
let set = extract_colours(cli.image);
|
||||||
let args: Vec<String> = 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]);
|
|
||||||
|
|
||||||
out_cols.iter().for_each(|(k, v)| {
|
if let Some(cs) = cli.colours {
|
||||||
println!(
|
let colours = map_image_list(&set, cs);
|
||||||
"({}, {}, {}): ({}, {}, {})",
|
pretty_print(colours);
|
||||||
k[0], k[1], k[2], v[0], v[1], v[2]
|
}
|
||||||
)
|
|
||||||
})
|
if let Some(i) = cli.second_image {
|
||||||
|
let colours = map_image_image(&set, i);
|
||||||
|
pretty_print(colours);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user