use crate::palette::Color;
use image::{DynamicImage, GenericImageView, ImageBuffer, RgbImage};
pub fn color_distance_sq(c1: (f32, f32, f32), c2: Color) -> f32 {
let dr = c1.0 - c2.r as f32;
let dg = c1.1 - c2.g as f32;
let db = c1.2 - c2.b as f32;
dr * dr + dg * dg + db * db
}
pub fn find_two_nearest(color: (f32, f32, f32), palette: &[Color]) -> (Color, Color, f32, f32) {
let mut best = palette[0];
let mut second = palette[0];
let mut best_dist = f32::MAX;
let mut second_dist = f32::MAX;
for &pal_color in palette {
let dist = color_distance_sq(color, pal_color);
if dist < best_dist {
second = best;
second_dist = best_dist;
best = pal_color;
best_dist = dist;
} else if dist < second_dist {
second = pal_color;
second_dist = dist;
}
}
(best, second, best_dist, second_dist)
}
pub fn apply_dithering(
img: &DynamicImage,
palette: &[Color],
bayer_matrix: &[Vec<f32>],
noise_intensity: f32,
) -> RgbImage {
let (width, height) = img.dimensions();
let matrix_size = bayer_matrix.len();
let mut output = ImageBuffer::new(width, height);
let rgb_img = img.to_rgb8();
for y in 0..height {
for x in 0..width {
let pixel = rgb_img.get_pixel(x, y);
let raw_threshold =
bayer_matrix[(y % matrix_size as u32) as usize][(x % matrix_size as u32) as usize];
let threshold =
(((raw_threshold - 0.5) * (1.0 + noise_intensity)).clamp(-0.5, 0.5)) + 0.5;
let src = (pixel[0] as f32, pixel[1] as f32, pixel[2] as f32);
let (best, second, best_dist, second_dist) = find_two_nearest(src, palette);
let chosen = if palette.len() == 1 || second_dist == f32::MAX {
best
} else {
let weight_nearest = second_dist / (best_dist + second_dist);
if threshold <= weight_nearest {
best
} else {
second
}
};
output.put_pixel(x, y, image::Rgb([chosen.r, chosen.g, chosen.b]));
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_distance_sq_same_color() {
let dist = color_distance_sq((100.0, 100.0, 100.0), Color::new(100, 100, 100));
assert!(dist.abs() < f32::EPSILON);
}
#[test]
fn test_color_distance_sq_black_to_white() {
let dist = color_distance_sq((0.0, 0.0, 0.0), Color::new(255, 255, 255));
let expected = 255.0 * 255.0 * 3.0;
assert!((dist - expected).abs() < 0.001);
}
#[test]
fn test_find_two_nearest_simple() {
let palette = vec![Color::new(0, 0, 0), Color::new(255, 255, 255)];
let (nearest, second, _, _) = find_two_nearest((50.0, 50.0, 50.0), &palette);
assert_eq!(nearest, Color::new(0, 0, 0)); assert_eq!(second, Color::new(255, 255, 255));
}
#[test]
fn test_find_two_nearest_bright() {
let palette = vec![Color::new(0, 0, 0), Color::new(255, 255, 255)];
let (nearest, second, _, _) = find_two_nearest((200.0, 200.0, 200.0), &palette);
assert_eq!(nearest, Color::new(255, 255, 255)); assert_eq!(second, Color::new(0, 0, 0));
}
#[test]
fn test_find_two_nearest_exact_match() {
let palette = vec![
Color::new(0, 0, 0),
Color::new(128, 128, 128),
Color::new(255, 255, 255),
];
let (nearest, _, dist, _) = find_two_nearest((128.0, 128.0, 128.0), &palette);
assert_eq!(nearest, Color::new(128, 128, 128));
assert!(dist < 0.001);
}
#[test]
fn test_apply_dithering_output_dimensions() {
use image::DynamicImage;
let img = DynamicImage::new_rgb8(10, 10);
let palette = vec![Color::new(0, 0, 0), Color::new(255, 255, 255)];
let bayer = crate::bayer::generate_bayer_matrix(0);
let output = apply_dithering(&img, &palette, &bayer, 1.0);
assert_eq!(output.width(), 10);
assert_eq!(output.height(), 10);
}
#[test]
fn test_apply_dithering_output_uses_palette_colors() {
use image::{DynamicImage, GenericImage, Rgba};
let mut img = DynamicImage::new_rgb8(4, 4);
for y in 0..4 {
for x in 0..4 {
img.put_pixel(x, y, Rgba([128, 128, 128, 255]));
}
}
let palette = vec![Color::new(0, 0, 0), Color::new(255, 255, 255)];
let bayer = crate::bayer::generate_bayer_matrix(0);
let output = apply_dithering(&img, &palette, &bayer, 1.0);
for y in 0..4 {
for x in 0..4 {
let pixel = output.get_pixel(x, y);
let is_black = pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0;
let is_white = pixel[0] == 255 && pixel[1] == 255 && pixel[2] == 255;
assert!(
is_black || is_white,
"Pixel at ({}, {}) is {:?}, expected black or white",
x,
y,
pixel
);
}
}
}
}