feat: init
This commit is contained in:
commit
127cb4c76d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
3029
Cargo.lock
generated
Normal file
3029
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "okthief"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
image = { version = "0.25.1", features = ["rayon"] }
|
||||
ok-picker = "0.0.3"
|
||||
rayon = "1.10.0"
|
||||
serde_json = "1.0.116"
|
257
src/main.rs
Normal file
257
src/main.rs
Normal file
@ -0,0 +1,257 @@
|
||||
use std::{collections::HashMap, f64::consts::PI, io::Cursor, iter::once};
|
||||
|
||||
use rayon::iter::ParallelIterator;
|
||||
|
||||
use clap::Parser as _;
|
||||
use ok_picker::colors::{OkHsl, Srgb};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Hue(u32);
|
||||
|
||||
impl Hue {
|
||||
pub fn from_okpicker(h: f64) -> Self {
|
||||
// f32::atan2 outputs (-pi, pi] radians
|
||||
let _2pi = 2.0 * PI;
|
||||
let h = if h < 0.0 { _2pi + h } else { h };
|
||||
|
||||
Self::from_ratio(h / _2pi)
|
||||
}
|
||||
|
||||
pub fn from_degrees(d: f64) -> Self {
|
||||
Self::from_ratio(d % 360.0)
|
||||
}
|
||||
|
||||
pub fn from_ratio(r: f64) -> Self {
|
||||
Hue((r * (u32::MAX as f64)) as u32)
|
||||
}
|
||||
|
||||
pub fn to_nearest(self, ratio: f64) -> Self {
|
||||
Self::from_ratio((self.ratio() / ratio).round() * ratio)
|
||||
}
|
||||
|
||||
pub fn ratio(self) -> f64 {
|
||||
(self.0 as f64) / (u32::MAX as f64)
|
||||
}
|
||||
|
||||
pub fn degrees(self) -> f64 {
|
||||
self.ratio() * 360.0
|
||||
}
|
||||
|
||||
pub fn radians(self) -> f64 {
|
||||
self.ratio() * 2.0 * PI
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct Args {
|
||||
/// Input image file
|
||||
///
|
||||
/// The image to generate colors from.
|
||||
///
|
||||
/// Supported formats:
|
||||
/// * PNG
|
||||
/// * JPEG
|
||||
/// * TIFF
|
||||
/// * BMP
|
||||
/// * APNG (1)
|
||||
/// * GIF (1)
|
||||
/// * WEBP (1)
|
||||
///
|
||||
/// (1) Only the first frame of animated images will be considered.
|
||||
#[arg(verbatim_doc_comment, short = 'f', long = "file")]
|
||||
file: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
main_impl().unwrap_or_else(|e| {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
fn main_impl() -> Result<(), String> {
|
||||
let args = Args::parse();
|
||||
let file = std::fs::read(args.file).map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
let (pixels, _) = image::io::Reader::new(Cursor::new(file))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| format!("{:?}", e))?
|
||||
.decode()
|
||||
.map_err(|e| format!("{:?}", e))?
|
||||
.thumbnail_exact(300, 300)
|
||||
.into_rgb8()
|
||||
.into_raw()
|
||||
.into_iter()
|
||||
.fold(
|
||||
(Vec::new(), (None, None, None)),
|
||||
|(mut pixels, (r, g, b)), v| match (r, g, b) {
|
||||
(Some(r), Some(g), Some(b)) => {
|
||||
pixels.push((r, g, b));
|
||||
(pixels, (Some(v), None, None))
|
||||
}
|
||||
(Some(r), Some(g), None) => (pixels, (Some(r), Some(g), Some(v))),
|
||||
(Some(r), None, None) => (pixels, (Some(r), Some(v), None)),
|
||||
(None, None, None) => (pixels, (Some(v), None, None)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
|
||||
let min_deltaf = 1.0 / 32.0;
|
||||
|
||||
let hues = pixels
|
||||
.into_iter()
|
||||
.fold(HashMap::<Hue, Vec<(f64, f64)>>::new(), |mut hues, p| {
|
||||
let hsl: OkHsl = Srgb {
|
||||
red: (p.0 as f64) / (u8::MAX as f64),
|
||||
green: (p.1 as f64) / (u8::MAX as f64),
|
||||
blue: (p.2 as f64) / (u8::MAX as f64),
|
||||
}
|
||||
.into();
|
||||
|
||||
let hue = Hue::from_okpicker(hsl.hue).to_nearest(min_deltaf);
|
||||
let sls = hues.entry(hue).or_default();
|
||||
let similar = sls.iter().any(|(s, l)| {
|
||||
(s - hsl.saturation).abs() <= min_deltaf && (l - hsl.lightness).abs() <= min_deltaf
|
||||
});
|
||||
if !similar {
|
||||
sls.push((hsl.saturation, hsl.lightness));
|
||||
}
|
||||
|
||||
hues
|
||||
});
|
||||
|
||||
let first = hues
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(h, sls)| (*h, sls[0].0, sls[0].1))
|
||||
.unwrap();
|
||||
let mut white = first;
|
||||
let mut black = first;
|
||||
|
||||
for (h, sls) in hues.iter() {
|
||||
for (s, l) in sls.iter() {
|
||||
let (h, s, l) = (*h, *s, *l);
|
||||
|
||||
let (_, _, wl) = white;
|
||||
let (_, _, bl) = black;
|
||||
|
||||
if l > wl {
|
||||
white = (h, s, l);
|
||||
}
|
||||
|
||||
if l < bl {
|
||||
black = (h, s, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
white.1 = white.1.min(0.7);
|
||||
white.2 = 0.95;
|
||||
black.2 = 0.2;
|
||||
|
||||
let lightness_ideal_delta = |l: f64| (0.6 - l).abs();
|
||||
|
||||
let mut colors = hues
|
||||
.into_iter()
|
||||
.flat_map(|(h, sls)| sls.into_iter().map(move |(s, l)| (h, s, l)))
|
||||
.filter(|(_, s, l)| *s >= 0.3 && *l <= 0.8 && *l >= 0.3)
|
||||
.fold(HashMap::<Hue, (f64, f64)>::new(), |mut map, (h, s, l)| {
|
||||
let ent = map.entry(h).or_insert((s, l));
|
||||
|
||||
if ent.0 < s && lightness_ideal_delta(l) < lightness_ideal_delta(ent.1) {
|
||||
*ent = (s, l);
|
||||
}
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
let proximity_to_true_red =
|
||||
|h: Hue| ((h.degrees() - 30.0).abs()).min((h.degrees() - 390.0).abs());
|
||||
|
||||
let red = colors
|
||||
.iter()
|
||||
.fold(None, |o, (h, (s, l))| {
|
||||
let (h, s, l) = (*h, *s, *l);
|
||||
match o {
|
||||
Some((h_, _, _)) if proximity_to_true_red(h_) >= proximity_to_true_red(h) => {
|
||||
Some((h, s, l))
|
||||
}
|
||||
Some(_) => o,
|
||||
None => Some((h, s, l)),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
colors.remove(&red.0);
|
||||
|
||||
let to_hex = |h: Hue, s: f64, l: f64| {
|
||||
let Srgb { red, green, blue } = Srgb::from(OkHsl {
|
||||
hue: h.radians(),
|
||||
saturation: s,
|
||||
lightness: l,
|
||||
});
|
||||
let (red, green, blue) = (
|
||||
((red * u8::MAX as f64) as u8),
|
||||
((green * u8::MAX as f64) as u8),
|
||||
((blue * u8::MAX as f64) as u8),
|
||||
);
|
||||
format!("#{:0>2x}{:0>2x}{:0>2x}", red, green, blue)
|
||||
};
|
||||
|
||||
if colors.len() < 6 {
|
||||
colors.clone().into_iter().for_each(|(h, (s, l))| {
|
||||
colors.insert(Hue::from_degrees(h.degrees() + 350.0), (s, l));
|
||||
colors.insert(Hue::from_degrees(h.degrees() + 10.0), (s, l));
|
||||
colors.insert(h, (s, l));
|
||||
})
|
||||
}
|
||||
|
||||
let colors_hex = once(white).chain(once(black)).chain(once(red))
|
||||
.chain(colors.iter().map(|(h, (s, l))| (*h, *s, *l)))
|
||||
.map(|(h, s, l)| {
|
||||
serde_json::json!({"okhsl": format!("({:.2}, {:.2}, {:.2})", h.degrees(), s, l), "hex": to_hex(h, s, l)})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
while colors.len() > 5 {
|
||||
let mut most_similar = Option::<(f64, Hue)>::None;
|
||||
for (h, (_, l)) in colors.iter() {
|
||||
for (h_, (_, l_)) in colors.iter() {
|
||||
if h == h_ {
|
||||
continue;
|
||||
}
|
||||
|
||||
let diff = (h_.degrees() - h.degrees())
|
||||
.abs()
|
||||
.min(((h_.degrees() + 360.0) - h.degrees()).abs())
|
||||
.min((h_.degrees() - (h.degrees() + 360.0)).abs());
|
||||
let to_remove = if l_ > l { *h } else { *h_ };
|
||||
if let Some((sd, sh)) = most_similar {
|
||||
if (sd == diff && h.ratio() > sh.ratio()) || sd > diff {
|
||||
most_similar = Some((diff, to_remove));
|
||||
}
|
||||
} else {
|
||||
most_similar = Some((diff, to_remove));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
colors.remove(&most_similar.unwrap().1);
|
||||
}
|
||||
|
||||
let colors = colors
|
||||
.into_iter()
|
||||
.map(|(h, (s, l))| (h, s, l.max(0.4)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let colors_hex = once(white).chain(once(black)).chain(once(red))
|
||||
.chain(colors.iter().map(|(h, s, l)| (*h, *s, *l)))
|
||||
.map(|(h, s, l)| {
|
||||
serde_json::json!({"okhsl": format!("({:.2}, {:.2}, {:.2})", h.degrees(), s, l), "hex": to_hex(h, s, l)})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
println!("{}", serde_json::to_string(&colors_hex).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user