diff --git a/src/imageproc.rs b/src/imageproc.rs index ac5f95f..e39d3d7 100644 --- a/src/imageproc.rs +++ b/src/imageproc.rs @@ -1,7 +1,7 @@ use image::RgbImage; -use palette::{cast::FromComponents, IntoColor, Lab, Oklch, Srgb}; use palette::color_difference::{Ciede2000, EuclideanDistance}; +use palette::{cast::FromComponents, IntoColor, Lab, Oklch, Srgb}; /// Palette used on the display; pixels can be one of these colors. /// @@ -16,18 +16,7 @@ const DISPLAY_PALETTE: [Srgb; 7] = [ Srgb::new(0.757, 0.443, 0.165), // Orange ]; -// fn octcolor_rgb(color: &OctColor) -> &Srgb { -// match color { -// OctColor::Black => &DISPLAY_PALETTE[0], -// OctColor::White => &DISPLAY_PALETTE[1], -// OctColor::Green => &DISPLAY_PALETTE[2], -// OctColor::Blue => &DISPLAY_PALETTE[3], -// OctColor::Red => &DISPLAY_PALETTE[4], -// OctColor::Yellow => &DISPLAY_PALETTE[5], -// OctColor::Orange => &DISPLAY_PALETTE[6], -// OctColor::HiZ => &DISPLAY_PALETTE[1], -// } -// } +// TODO: support different color palettes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DisplayColor { @@ -94,7 +83,7 @@ impl EInkBuffer { // pub fn make_image(&self) -> RgbImage { // RgbImage::from_fn(800, 480, |x, y| { // let srgb = Srgb::from(self.0[y * 800 + x]); - // }) + // }) // } } @@ -121,17 +110,9 @@ impl EInkBuffer { // } pub trait Ditherer { - fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer); + fn dither(&self, img: &RgbImage, output: &mut EInkBuffer); } -// fn color_distance(c1: LinSrgb, c2: LinSrgb) -> f32 { -// let r2 = (c2.red - c1.red).powf(2.0); -// let g2 = (c2.green - c1.green).powf(2.0); -// let b2 = (c2.blue - c1.blue).powf(2.0); -// -// (r2 + g2 + b2).sqrt() -// } - /// Find the closest approximate palette color to the given sRGB value. /// This uses euclidian distance in linear space. #[must_use] @@ -151,7 +132,7 @@ pub fn nearest_neighbor(input_color: Lab) -> (DisplayColor, Lab) { pub struct NNDither(); impl Ditherer for NNDither { - fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer) { + fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) { assert!(img.width() == 800); assert!(img.height() == 480); @@ -165,8 +146,6 @@ impl Ditherer for NNDither { } } -pub struct FloydSteinbergDither(); - /// Compute the vector index for a given image by using the size of rows. Assumes that images /// are indexed in row-major order. const fn coord_to_idx(x: u32, y: u32, xsize: u32) -> usize { @@ -174,7 +153,7 @@ const fn coord_to_idx(x: u32, y: u32, xsize: u32) -> usize { } /// Compute the error-adjusted new lab value based on the error value of the currently scanned -/// pixel, plus a scalar factor. +/// pixel multiplied by a scalar factor. fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab { let (p_l, p_a, p_b) = orig.into_components(); let (err_l, err_a, err_b) = err.into_components(); @@ -185,6 +164,9 @@ fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab { )) } + +/// ``DiffusionPoint`` is part of the diffusion matrix, represented by a shift in x and y and an error +/// scaling factor. struct DiffusionPoint { xshift: i32, yshift: i32, @@ -192,6 +174,7 @@ struct DiffusionPoint { } impl DiffusionPoint { + /// Creates a new ``DiffusionPoint`` const fn new(xshift: i32, yshift: i32, scale: f32) -> Self { Self { xshift, @@ -201,23 +184,78 @@ impl DiffusionPoint { } } -const FLOYD_STEINBERG: [DiffusionPoint; 4] = [ +static FLOYD_STEINBERG_POINTS: &[DiffusionPoint] = &[ DiffusionPoint::new(1, 0, 7.0 / 16.0), DiffusionPoint::new(-1, 1, 3.0 / 16.0), DiffusionPoint::new(0, 1, 5.0 / 16.0), DiffusionPoint::new(1, 1, 1.0 / 16.0), ]; -const ATKINSON_DITHER: [DiffusionPoint; 6] = [ - DiffusionPoint::new(1, 0, 1.0/8.0), - DiffusionPoint::new(2, 0, 1.0/8.0), - DiffusionPoint::new(-1, 1, 1.0/8.0), - DiffusionPoint::new(0, 1, 1.0/8.0), - DiffusionPoint::new(1, 1, 1.0/8.0), - DiffusionPoint::new(0, 2, 1.0/8.0), + +static ATKINSON_DITHER_POINTS: &[DiffusionPoint] = &[ + DiffusionPoint::new(1, 0, 1.0 / 8.0), + DiffusionPoint::new(2, 0, 1.0 / 8.0), + DiffusionPoint::new(-1, 1, 1.0 / 8.0), + DiffusionPoint::new(0, 1, 1.0 / 8.0), + DiffusionPoint::new(1, 1, 1.0 / 8.0), + DiffusionPoint::new(0, 2, 1.0 / 8.0), ]; -impl Ditherer for FloydSteinbergDither { - fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer) { +static SIERRA_DITHER_POINTS: &[DiffusionPoint] = &[ + DiffusionPoint::new(1, 0, 5.0 / 32.0), + DiffusionPoint::new(2, 0, 3.0 / 32.0), + DiffusionPoint::new(-2, 1, 2.0 / 32.0), + DiffusionPoint::new(-1, 1, 4.0 / 32.0), + DiffusionPoint::new(0, 1, 5.0 / 32.0), + DiffusionPoint::new(1, 1, 4.0 / 32.0), + DiffusionPoint::new(2, 1, 2.0 / 32.0), + DiffusionPoint::new(-1, 2, 2.0 / 32.0), + DiffusionPoint::new(0, 2, 3.0 / 32.0), + DiffusionPoint::new(1, 2, 2.0 / 32.0), +]; + +static STUKI_DITHER_POINTS: &[DiffusionPoint] = &[ + DiffusionPoint::new(1, 0, 8.0 / 42.0), + DiffusionPoint::new(2, 0, 4.0 / 42.0), + DiffusionPoint::new(-2, 1, 2.0 / 42.0), + DiffusionPoint::new(-1, 1, 4.0 / 42.0), + DiffusionPoint::new(0, 1, 8.0 / 42.0), + DiffusionPoint::new(1, 1, 4.0 / 42.0), + DiffusionPoint::new(2, 1, 2.0 / 42.0), + DiffusionPoint::new(-2, 2, 1.0 / 42.0), + DiffusionPoint::new(-1, 2, 2.0 / 42.0), + DiffusionPoint::new(0, 2, 4.0 / 42.0), + DiffusionPoint::new(1, 2, 2.0 / 42.0), + DiffusionPoint::new(1, 2, 1.0 / 42.0), +]; + +pub enum DiffusionMatrix { + FloydSteinberg, + Atkinson, + Sierra, + Stuki, +} + +impl DiffusionMatrix { + fn value(&self) -> &'static [DiffusionPoint] { + match *self { + Self::FloydSteinberg => FLOYD_STEINBERG_POINTS, + Self::Atkinson => ATKINSON_DITHER_POINTS, + Self::Sierra => SIERRA_DITHER_POINTS, + Self::Stuki => STUKI_DITHER_POINTS, + } + } +} + +pub struct ErrorDiffusionDither(DiffusionMatrix); +impl ErrorDiffusionDither { + #[must_use] + pub const fn new(dm: DiffusionMatrix) -> Self { + Self(dm) + } +} + +impl Ditherer for ErrorDiffusionDither { + fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) { // create a copy of the image in Lab space, mutable. let srgb = <&[Srgb]>::from_components(&**img); let (xsize, ysize) = img.dimensions(); @@ -235,18 +273,14 @@ impl Ditherer for FloydSteinbergDither { // set the color in the output buffer. output.0[index] = nearest; // take the error, and propagate it. - for point in ATKINSON_DITHER { + for point in self.0.value() { let Some(target_x) = x.checked_add_signed(point.xshift) else { continue; }; let Some(target_y) = y.checked_add_signed(point.yshift) else { continue; }; - let target = coord_to_idx( - target_x, - target_y, - xsize, - ); + let target = coord_to_idx(target_x, target_y, xsize); if let Some(pix) = temp_img.get(target) { temp_img[target] = get_error_adjusted(pix, &err, point.scale); } diff --git a/src/main.rs b/src/main.rs index 4cba245..7cf56bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ pub mod display; pub mod imageproc; use crate::display::Wrapper; -use crate::imageproc::{Ditherer, EInkBuffer, FloydSteinbergDither, NNDither}; +use crate::imageproc::{DiffusionMatrix, EInkBuffer, ErrorDiffusionDither, Ditherer}; use clap::{Parser, Subcommand}; use image::RgbImage; @@ -36,7 +36,7 @@ fn main() -> anyhow::Result<()> { let mut display = Wrapper::new()?; let mut eink_buf = EInkBuffer::new(800, 480); - let mut dither = FloydSteinbergDither{}; + let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); dither.dither(&img, &mut eink_buf); let raw_buf = eink_buf.into_display_buffer(); @@ -52,7 +52,7 @@ fn main() -> anyhow::Result<()> { if matches!(cli.command, Command::Convert) { let img: RgbImage = image::io::Reader::open("myimage.png")?.decode()?.into(); let mut eink_buf = EInkBuffer::new(800, 480); - let mut dither = FloydSteinbergDither{}; + let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); dither.dither(&img, &mut eink_buf); let raw_buf = eink_buf.into_display_buffer();