feat: init

This commit is contained in:
orion 2024-04-19 13:29:30 -05:00
commit 127cb4c76d
Signed by: orion
GPG Key ID: 6D4165AE4C928719
4 changed files with 3300 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

3029
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View 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
View 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(())
}