diff --git a/Cargo.lock b/Cargo.lock index 1481c8f..827513b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,8 +187,6 @@ dependencies = [ [[package]] name = "image" version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b0553fec6407d63fe2975b794dfb099f3f790bdc958823851af37b26404ab4" dependencies = [ "bytemuck", "byteorder", @@ -325,6 +323,7 @@ dependencies = [ "structopt", "svg", "thiserror", + "x11colors", ] [[package]] @@ -569,3 +568,10 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x11colors" +version = "0.1.0" +dependencies = [ + "lazy_static", +] diff --git a/Cargo.toml b/Cargo.toml index 3c9a072..5adff5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -image = "0.23.6" +#image = "0.23.6" +image = {path="../../github.com/wathiede/image"} log = "0.4.8" svg = "0.8.0" structopt = "0.3.15" thiserror = "1.0.20" anyhow = "1.0.31" +#x11colors = {git="https://git.z.xinu.tv/wathiede/x11colors"} +x11colors = {path="../../xinu.tv/x11colors"} [dev-dependencies] lazy_static = "1.4.0" diff --git a/src/bin/debugger.rs b/src/bin/debugger.rs new file mode 100644 index 0000000..127c932 --- /dev/null +++ b/src/bin/debugger.rs @@ -0,0 +1,134 @@ +use std::cmp::Ordering; +use std::collections::HashMap; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::Result; +use image::imageops::colorops::{index_colors, ColorMap}; +use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Rgba}; +use structopt::StructOpt; +use x11colors::COLORS; + +use perler::NearestColor; + +#[derive(Debug, StructOpt)] +enum Command { + Nearest { + /// Input image file. + input: PathBuf, + /// Output of processed image. + output: PathBuf, + }, + Generate { + /// Output of processed image. + output: PathBuf, + }, +} + +/// Convert image to SVG in the style of perler instructions. +#[derive(StructOpt, Debug)] +#[structopt(name = "perler")] +struct Opt { + #[structopt(subcommand)] + cmd: Command, +} + +fn print_stats(img: &DynamicImage) { + let mut h = HashMap::new(); + for (_x, _y, p) in img.pixels() { + let c = h.entry(p).or_insert(0); + *c += 1; + } + let mut lines = HashMap::>>::new(); + for col in h.keys() { + lines.entry(col.0[0]).or_insert(vec![]).push(*col); + } + let mut grp: Vec<_> = lines.keys().cloned().collect(); + let lines = lines; + grp.sort(); + println!("({}) Colors:", h.len()); + for g in grp { + let mut keys: Vec<_> = lines[&g].iter().cloned().collect(); + keys.sort_by(|l, r| { + let [l_r, l_g, l_b, _] = l.0; + let [r_r, r_g, r_b, _] = r.0; + match l_r.cmp(&r_r) { + Ordering::Less => return Ordering::Less, + Ordering::Equal => match l_g.cmp(&r_g) { + Ordering::Less => return Ordering::Less, + Ordering::Equal => return l_b.cmp(&r_b), + Ordering::Greater => return Ordering::Greater, + }, + Ordering::Greater => return Ordering::Greater, + } + }); + let mut out = Vec::new(); + for col in keys { + let cnt = h[&col]; + write!( + out, + " #{:02x}{:02x}{:02x}: {}", + col.0[0], col.0[1], col.0[2], cnt + ) + .unwrap(); + } + println!("{}", std::str::from_utf8(&out).unwrap()); + } +} + +fn generate(output: PathBuf) -> Result<()> { + let w = 256; + let h = 300; + let mut img = DynamicImage::new_rgba8(w, h); + for y in 0..50 { + for x in 0..w { + img.put_pixel(x, y, [x as u8, 0, 0, 255].into()); + img.put_pixel(x, 50 + y, [x as u8, x as u8, 0, 255].into()); + img.put_pixel(x, 100 + y, [0, x as u8, 0, 255].into()); + img.put_pixel(x, 150 + y, [0, x as u8, x as u8, 255].into()); + img.put_pixel(x, 200 + y, [0, 0, x as u8, 255].into()); + img.put_pixel(x, 250 + y, [x as u8, 0, x as u8, 255].into()); + } + } + img.save(output)?; + Ok(()) +} + +fn nearest(input: PathBuf, output: PathBuf) -> Result<()> { + let img = image::open(input)?; + let (w, h) = &img.dimensions(); + println!("Before:"); + print_stats(&img); + let mut img2 = img.to_rgba(); + //let cmap = NeuQuant::new(1, 256, &raw); + let mut palette: Vec<_> = COLORS + .iter() + .cloned() + .map(|[r, g, b]| [r, g, b, 255].into()) + .collect(); + palette.sort_by_key(|Rgba([r, g, b, a])| r << 24 | g << 16 | b << 8 | a); + let cmap = NearestColor::new(palette); + let pal = index_colors(&mut img2, &cmap); + let out_buf = ImageBuffer::from_fn(*w, *h, |x, y| -> Rgba { + let p = pal.get_pixel(x, y); + cmap.lookup(p.0[0] as usize) + .expect(&format!("({}) indexed color out-of-range", p.0[0])) + .into() + }); + let out = DynamicImage::ImageRgba8(out_buf); + + println!("After:"); + print_stats(&out); + out.save(output)?; + + Ok(()) +} + +fn main() -> Result<()> { + let opt = Opt::from_args(); + + match opt.cmd { + Command::Nearest { input, output } => nearest(input, output), + Command::Generate { output } => generate(output), + } +} diff --git a/src/lib.rs b/src/lib.rs index efdb0b5..4fe177e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ +use std::collections::HashMap; + +use image::imageops::colorops::ColorMap; +use image::Rgba; use image::{DynamicImage, GenericImage, GenericImageView}; use log::debug; use svg::node::element::Group; @@ -17,6 +21,55 @@ pub enum PerlerError { }, } +#[derive(Default, Debug)] +pub struct NearestColor { + palette: Vec>, + color_cache: HashMap, usize>, +} + +impl NearestColor { + pub fn new(palette: Vec>) -> NearestColor { + NearestColor { + palette, + color_cache: HashMap::new(), + } + } +} + +impl ColorMap for NearestColor { + type Color = Rgba; + + fn index_of(&self, color: &Self::Color) -> usize { + // TODO(wathiede): cache hits. + let mut min_idx = usize::MAX; + let mut min_dist = i32::MAX; + for (idx, rgb) in self.palette.iter().enumerate() { + let [r1, g1, b1, a1] = color.0; + let [r1, g1, b1, a1] = [r1 as i32, g1 as i32, b1 as i32, a1 as i32]; + let [r2, g2, b2, a2] = rgb.0; + let [r2, g2, b2, a2] = [r2 as i32, g2 as i32, b2 as i32, a2 as i32]; + let dist = (r2 - r1) * (r2 - r1) + + (g2 - g1) * (g2 - g1) + + (b2 - b1) * (b2 - b1) + + (a2 - a1) * (a2 - a1); + + //dbg!((dist, min_dist, idx, min_idx)); + if dist < min_dist { + min_dist = dist; + min_idx = idx; + } + } + min_idx + } + fn map_color(&self, color: &mut Self::Color) { + *color = self.palette[self.index_of(color)]; + } + + fn lookup(&self, idx: usize) -> Option { + self.palette.get(idx).cloned() + } +} + pub fn svgify(input: &DynamicImage) -> Result { let dim = input.dimensions(); let (w, h) = (dim.0 as usize, dim.1 as usize); @@ -274,4 +327,18 @@ mod tests { compare_images(want, got); } } + + #[test] + fn test_color_map() { + let palette = vec![[0, 0, 0, 255], [255, 255, 255, 255], [255, 0, 0, 255]] + .into_iter() + .map(|rgb| rgb.into()) + .collect(); + let cmap = NearestColor::new(palette); + let got = cmap.index_of(&[255, 10, 10, 255].into()); + assert_eq!(got, 2); + + let got = cmap.lookup(2); + assert_eq!(got, Some([255, 0, 0, 255].into())); + } }