use image::RgbImage; use palette::color_difference::{Ciede2000, EuclideanDistance}; use palette::{cast::FromComponents, IntoColor, Lab, Oklch, Srgb}; use image::Rgb as imgRgb; /// Palette used on the display; pixels can be one of these colors. /// /// The RGB values are slightly adjusted to improve accuracy. const DISPLAY_PALETTE: [Srgb; 7] = [ Srgb::new(0.047, 0.047, 0.055), // Black Srgb::new(0.824, 0.824, 0.816), // White Srgb::new(0.118, 0.376, 0.122), // Green Srgb::new(0.114, 0.118, 0.667), // Blue Srgb::new(0.549, 0.106, 0.114), // Red Srgb::new(0.827, 0.788, 0.239), // Yellow Srgb::new(0.757, 0.443, 0.165), // Orange ]; // TODO: support different color palettes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DisplayColor { Black, White, Green, Blue, Red, Yellow, Orange, } impl From for Srgb { fn from(value: DisplayColor) -> Self { DISPLAY_PALETTE[value as usize] } } impl DisplayColor { fn from_u8(value: u8) -> Self { match value { 0 => Self::Black, 1 => Self::White, 2 => Self::Green, 3 => Self::Blue, 4 => Self::Red, 5 => Self::Yellow, 6 => Self::Orange, _ => panic!("unexpected DisplayColor {value}"), } } fn into_byte(color1: Self, color2: Self) -> u8 { let upper: u8 = color1.into(); let lower: u8 = color2.into(); upper << 4 | lower } } impl From for u8 { fn from(value: DisplayColor) -> Self { value as Self } } /// Buffer to be sent to the ``EInk`` display. #[derive(Debug)] pub struct EInkBuffer(Vec); impl EInkBuffer { #[must_use] pub fn into_display_buffer(&self) -> Vec { let mut buf = Vec::with_capacity(self.0.len() / 2); for colors in self.0.chunks_exact(2) { buf.push(DisplayColor::into_byte(colors[0], colors[1])); } buf } #[must_use] pub fn new(width: usize, height: usize) -> Self { let v = vec![DisplayColor::Black; width * height]; Self(v) } pub fn make_image(&self) -> RgbImage { RgbImage::from_fn(800, 480, |x, y| { let srgb = Srgb::from(self.0[(y * 800 + x) as usize]); let arr: [u8; 3] = srgb.into_format().into(); imgRgb(arr) }) } } pub trait Ditherer { fn dither(&self, img: &RgbImage, output: &mut EInkBuffer); } /// Find the closest approximate palette color to the given sRGB value. /// This uses euclidian distance in linear space. #[must_use] pub fn nearest_neighbor(input_color: Lab) -> (DisplayColor, Lab) { let (nearest, _, color_diff) = DISPLAY_PALETTE .iter() .enumerate() .map(|(idx, p_color)| { let c: Lab = (*p_color).into_color(); (idx, input_color.difference(c), input_color - c) }) .min_by(|(_, a, _), (_, b, _)| a.total_cmp(b)) .expect("could not find a color"); (DisplayColor::from_u8(nearest as u8), color_diff) } pub struct NNDither(); impl Ditherer for NNDither { fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) { assert!(img.width() == 800); assert!(img.height() == 480); // sRGB view into the given image. zero copy! let srgb = <&[Srgb]>::from_components(&**img); for (idx, pixel) in srgb.iter().enumerate() { let (n, _) = nearest_neighbor(pixel.into_format().into_color()); output.0[idx] = n; } } } /// 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 { (y * xsize + x) as usize } /// Compute the error-adjusted new lab value based on the error value of the currently scanned /// 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(); Lab::from_components(( p_l + err_l * scalar, p_a + err_a * scalar, p_b + err_b * scalar, )) } /// ``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, scale: f32, } impl DiffusionPoint { /// Creates a new ``DiffusionPoint`` const fn new(xshift: i32, yshift: i32, scale: f32) -> Self { Self { xshift, yshift, scale, } } } 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), ]; 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), ]; 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(); let mut temp_img: Vec = Vec::with_capacity((xsize * ysize) as usize); for pix in srgb { temp_img.push(pix.into_format().into_color()); } // now we take our units. for y in 0..ysize { for x in 0..xsize { let index = coord_to_idx(x, y, xsize); let curr_pix = temp_img[index]; let (nearest, err) = nearest_neighbor(curr_pix); // set the color in the output buffer. output.0[index] = nearest; // take the error, and propagate it. 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); if let Some(pix) = temp_img.get(target) { temp_img[target] = get_error_adjusted(pix, &err, point.scale); } } } } } }