#![allow(clippy::doc_markdown)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::float_cmp)]
#![allow(clippy::uninlined_format_args)]
use image::GrayImage;
use tracing::debug;
use crate::error::DotmaxError;
use crate::image::threshold::{auto_threshold, BinaryImage};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DitheringMethod {
None,
FloydSteinberg,
Bayer,
Atkinson,
}
const BAYER_MATRIX_8X8: [[u8; 8]; 8] = [
[0, 32, 8, 40, 2, 34, 10, 42],
[48, 16, 56, 24, 50, 18, 58, 26],
[12, 44, 4, 36, 14, 46, 6, 38],
[60, 28, 52, 20, 62, 30, 54, 22],
[3, 35, 11, 43, 1, 33, 9, 41],
[51, 19, 59, 27, 49, 17, 57, 25],
[15, 47, 7, 39, 13, 45, 5, 37],
[63, 31, 55, 23, 61, 29, 53, 21],
];
const THRESHOLD: u8 = 127;
pub fn apply_dithering(
gray: &GrayImage,
method: DitheringMethod,
) -> Result<BinaryImage, DotmaxError> {
apply_dithering_with_custom_threshold(gray, method, None)
}
pub fn apply_dithering_with_custom_threshold(
gray: &GrayImage,
method: DitheringMethod,
threshold: Option<u8>,
) -> Result<BinaryImage, DotmaxError> {
let threshold_value = threshold.unwrap_or(THRESHOLD);
debug!(
"Applying {:?} dithering to {}×{} image with threshold {}",
method,
gray.width(),
gray.height(),
threshold_value
);
match method {
DitheringMethod::None => {
let binary = auto_threshold(&image::DynamicImage::ImageLuma8(gray.clone()));
Ok(binary)
}
DitheringMethod::FloydSteinberg => floyd_steinberg_with_threshold(gray, threshold_value),
DitheringMethod::Bayer => bayer(gray), DitheringMethod::Atkinson => atkinson_with_threshold(gray, threshold_value),
}
}
pub fn floyd_steinberg(gray: &GrayImage) -> Result<BinaryImage, DotmaxError> {
floyd_steinberg_with_threshold(gray, THRESHOLD)
}
fn floyd_steinberg_with_threshold(
gray: &GrayImage,
threshold: u8,
) -> Result<BinaryImage, DotmaxError> {
let width = gray.width() as usize;
let height = gray.height() as usize;
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidParameter {
parameter_name: "image dimensions".to_string(),
value: format!("{}×{}", width, height),
min: "1×1".to_string(),
max: "unlimited".to_string(),
});
}
debug!("Floyd-Steinberg dithering {}×{} image", width, height);
let mut errors = vec![0.0f32; width * height];
let mut binary = BinaryImage::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
let pixel_idx = y * width + x;
let old_pixel = gray.get_pixel(x as u32, y as u32)[0] as f32;
let new_pixel = old_pixel + errors[pixel_idx];
let output_value = if new_pixel >= threshold as f32 {
255.0
} else {
0.0
};
binary.set_pixel(x as u32, y as u32, output_value == 255.0);
let quant_error = new_pixel - output_value;
if x + 1 < width {
errors[pixel_idx + 1] += quant_error * 7.0 / 16.0;
}
if y + 1 < height {
let next_row_idx = (y + 1) * width;
if x > 0 {
errors[next_row_idx + x - 1] += quant_error * 3.0 / 16.0;
}
errors[next_row_idx + x] += quant_error * 5.0 / 16.0;
if x + 1 < width {
errors[next_row_idx + x + 1] += quant_error * 1.0 / 16.0;
}
}
}
}
Ok(binary)
}
pub fn bayer(gray: &GrayImage) -> Result<BinaryImage, DotmaxError> {
let width = gray.width() as usize;
let height = gray.height() as usize;
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidParameter {
parameter_name: "image dimensions".to_string(),
value: format!("{}×{}", width, height),
min: "1×1".to_string(),
max: "unlimited".to_string(),
});
}
debug!("Bayer dithering {}×{} image", width, height);
let mut binary = BinaryImage::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
let pixel_value = gray.get_pixel(x as u32, y as u32)[0];
let bayer_threshold = BAYER_MATRIX_8X8[y % 8][x % 8] as f32 / 64.0;
let normalized_pixel = pixel_value as f32 / 255.0;
let output = normalized_pixel > bayer_threshold;
binary.set_pixel(x as u32, y as u32, output);
}
}
Ok(binary)
}
pub fn atkinson(gray: &GrayImage) -> Result<BinaryImage, DotmaxError> {
atkinson_with_threshold(gray, THRESHOLD)
}
fn atkinson_with_threshold(gray: &GrayImage, threshold: u8) -> Result<BinaryImage, DotmaxError> {
let width = gray.width() as usize;
let height = gray.height() as usize;
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidParameter {
parameter_name: "image dimensions".to_string(),
value: format!("{}×{}", width, height),
min: "1×1".to_string(),
max: "unlimited".to_string(),
});
}
debug!("Atkinson dithering {}×{} image", width, height);
let mut errors = vec![0.0f32; width * height];
let mut binary = BinaryImage::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
let pixel_idx = y * width + x;
let old_pixel = gray.get_pixel(x as u32, y as u32)[0] as f32;
let new_pixel = old_pixel + errors[pixel_idx];
let output_value = if new_pixel >= threshold as f32 {
255.0
} else {
0.0
};
binary.set_pixel(x as u32, y as u32, output_value == 255.0);
let quant_error = new_pixel - output_value;
if x + 1 < width {
errors[pixel_idx + 1] += quant_error / 8.0;
}
if x + 2 < width {
errors[pixel_idx + 2] += quant_error / 8.0;
}
if y + 1 < height {
let next_row_idx = (y + 1) * width;
if x > 0 {
errors[next_row_idx + x - 1] += quant_error / 8.0;
}
errors[next_row_idx + x] += quant_error / 8.0;
if x + 1 < width {
errors[next_row_idx + x + 1] += quant_error / 8.0;
}
}
if y + 2 < height {
let two_rows_idx = (y + 2) * width;
errors[two_rows_idx + x] += quant_error / 8.0;
}
}
}
Ok(binary)
}
#[cfg(test)]
mod tests {
use super::*;
use image::{GrayImage, Luma};
fn create_uniform_image(width: u32, height: u32, value: u8) -> GrayImage {
GrayImage::from_fn(width, height, |_, _| Luma([value]))
}
fn create_gradient_image(width: u32, height: u32) -> GrayImage {
GrayImage::from_fn(width, height, |x, _| {
let value = (x as f32 / width as f32 * 255.0) as u8;
Luma([value])
})
}
#[test]
fn test_dithering_method_enum() {
let _ = DitheringMethod::None;
let _ = DitheringMethod::FloydSteinberg;
let _ = DitheringMethod::Bayer;
let _ = DitheringMethod::Atkinson;
}
#[test]
fn test_dithering_method_derives() {
let method1 = DitheringMethod::FloydSteinberg;
let method2 = DitheringMethod::FloydSteinberg;
let method3 = DitheringMethod::Bayer;
assert_eq!(format!("{:?}", method1), "FloydSteinberg");
#[allow(clippy::clone_on_copy)]
let method1_clone = method1.clone();
assert_eq!(method1, method1_clone);
let method1_copy = method1;
assert_eq!(method1, method1_copy);
assert_eq!(method1, method2);
assert_ne!(method1, method3);
}
#[test]
fn test_floyd_steinberg_uniform_gray() {
let gray = create_uniform_image(10, 10, 128);
let binary = floyd_steinberg(&gray).unwrap();
let black_count = binary.pixels.iter().filter(|&&p| p).count();
let total = binary.pixels.len();
assert!(
(black_count as f32 / total as f32) > 0.3 && (black_count as f32 / total as f32) < 0.7,
"Expected balanced black/white for uniform gray, got {} black out of {}",
black_count,
total
);
}
#[test]
fn test_floyd_steinberg_all_black() {
let gray = create_uniform_image(10, 10, 0);
let binary = floyd_steinberg(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| !p),
"Expected all black for input value 0"
);
}
#[test]
fn test_floyd_steinberg_all_white() {
let gray = create_uniform_image(10, 10, 255);
let binary = floyd_steinberg(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| p),
"Expected all white for input value 255"
);
}
#[test]
fn test_floyd_steinberg_gradient() {
let gray = create_gradient_image(100, 10);
let binary = floyd_steinberg(&gray).unwrap();
let left_quarter = &binary.pixels[0..250]; let right_quarter = &binary.pixels[750..1000];
let left_black = left_quarter.iter().filter(|&&p| !p).count();
let right_black = right_quarter.iter().filter(|&&p| !p).count();
assert!(
left_black > right_black,
"Expected more black on left (dark) side, got left={} right={}",
left_black,
right_black
);
}
#[test]
fn test_floyd_steinberg_small_image() {
let gray = create_uniform_image(1, 1, 128);
let binary = floyd_steinberg(&gray).unwrap();
assert_eq!(binary.width, 1);
assert_eq!(binary.height, 1);
let gray = create_uniform_image(2, 2, 128);
let binary = floyd_steinberg(&gray).unwrap();
assert_eq!(binary.width, 2);
assert_eq!(binary.height, 2);
let gray = create_uniform_image(3, 3, 128);
let binary = floyd_steinberg(&gray).unwrap();
assert_eq!(binary.width, 3);
assert_eq!(binary.height, 3);
}
#[test]
fn test_floyd_steinberg_zero_dimensions() {
let gray = GrayImage::new(0, 10);
assert!(floyd_steinberg(&gray).is_err());
let gray = GrayImage::new(10, 0);
assert!(floyd_steinberg(&gray).is_err());
let gray = GrayImage::new(0, 0);
assert!(floyd_steinberg(&gray).is_err());
}
#[test]
fn test_bayer_uniform_gray() {
let gray = create_uniform_image(16, 16, 128);
let binary = bayer(&gray).unwrap();
let black_count = binary.pixels.iter().filter(|&&p| p).count();
let total = binary.pixels.len();
assert!(
(black_count as f32 / total as f32) > 0.3 && (black_count as f32 / total as f32) < 0.7,
"Expected balanced black/white for uniform gray, got {} black out of {}",
black_count,
total
);
}
#[test]
fn test_bayer_all_black() {
let gray = create_uniform_image(16, 16, 0);
let binary = bayer(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| !p),
"Expected all black for input value 0"
);
}
#[test]
fn test_bayer_all_white() {
let gray = create_uniform_image(16, 16, 255);
let binary = bayer(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| p),
"Expected all white for input value 255"
);
}
#[test]
fn test_bayer_deterministic() {
let gray = create_uniform_image(10, 10, 128);
let binary1 = bayer(&gray).unwrap();
let binary2 = bayer(&gray).unwrap();
assert_eq!(
binary1.pixels, binary2.pixels,
"Bayer should be deterministic"
);
}
#[test]
fn test_bayer_pattern_applied() {
let gray = create_uniform_image(8, 8, 128);
let binary = bayer(&gray).unwrap();
let black_count = binary.pixels.iter().filter(|&&p| p).count();
assert!(
black_count > 0 && black_count < binary.pixels.len(),
"Bayer pattern should produce mixed output for uniform gray"
);
}
#[test]
fn test_bayer_small_image() {
let gray = create_uniform_image(1, 1, 128);
let binary = bayer(&gray).unwrap();
assert_eq!(binary.width, 1);
assert_eq!(binary.height, 1);
let gray = create_uniform_image(2, 2, 128);
let binary = bayer(&gray).unwrap();
assert_eq!(binary.width, 2);
assert_eq!(binary.height, 2);
}
#[test]
fn test_bayer_zero_dimensions() {
let gray = GrayImage::new(0, 10);
assert!(bayer(&gray).is_err());
let gray = GrayImage::new(10, 0);
assert!(bayer(&gray).is_err());
}
#[test]
fn test_atkinson_uniform_gray() {
let gray = create_uniform_image(10, 10, 128);
let binary = atkinson(&gray).unwrap();
let black_count = binary.pixels.iter().filter(|&&p| p).count();
let total = binary.pixels.len();
assert!(
(black_count as f32 / total as f32) > 0.3 && (black_count as f32 / total as f32) < 0.7,
"Expected balanced black/white for uniform gray, got {} black out of {}",
black_count,
total
);
}
#[test]
fn test_atkinson_all_black() {
let gray = create_uniform_image(10, 10, 0);
let binary = atkinson(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| !p),
"Expected all black for input value 0"
);
}
#[test]
fn test_atkinson_all_white() {
let gray = create_uniform_image(10, 10, 255);
let binary = atkinson(&gray).unwrap();
assert!(
binary.pixels.iter().all(|&p| p),
"Expected all white for input value 255"
);
}
#[test]
fn test_atkinson_gradient() {
let gray = create_gradient_image(100, 10);
let binary = atkinson(&gray).unwrap();
let left_quarter = &binary.pixels[0..250]; let right_quarter = &binary.pixels[750..1000];
let left_black = left_quarter.iter().filter(|&&p| !p).count();
let right_black = right_quarter.iter().filter(|&&p| !p).count();
assert!(
left_black > right_black,
"Expected more black on left (dark) side, got left={} right={}",
left_black,
right_black
);
}
#[test]
fn test_atkinson_small_image() {
let gray = create_uniform_image(1, 1, 128);
let binary = atkinson(&gray).unwrap();
assert_eq!(binary.width, 1);
assert_eq!(binary.height, 1);
let gray = create_uniform_image(2, 2, 128);
let binary = atkinson(&gray).unwrap();
assert_eq!(binary.width, 2);
assert_eq!(binary.height, 2);
}
#[test]
fn test_atkinson_zero_dimensions() {
let gray = GrayImage::new(0, 10);
assert!(atkinson(&gray).is_err());
let gray = GrayImage::new(10, 0);
assert!(atkinson(&gray).is_err());
}
#[test]
fn test_apply_dithering_none() {
let gray = create_uniform_image(10, 10, 128);
let binary = apply_dithering(&gray, DitheringMethod::None).unwrap();
assert_eq!(binary.width, 10);
assert_eq!(binary.height, 10);
}
#[test]
fn test_apply_dithering_floyd_steinberg() {
let gray = create_uniform_image(10, 10, 128);
let binary = apply_dithering(&gray, DitheringMethod::FloydSteinberg).unwrap();
assert_eq!(binary.width, 10);
assert_eq!(binary.height, 10);
}
#[test]
fn test_apply_dithering_bayer() {
let gray = create_uniform_image(10, 10, 128);
let binary = apply_dithering(&gray, DitheringMethod::Bayer).unwrap();
assert_eq!(binary.width, 10);
assert_eq!(binary.height, 10);
}
#[test]
fn test_apply_dithering_atkinson() {
let gray = create_uniform_image(10, 10, 128);
let binary = apply_dithering(&gray, DitheringMethod::Atkinson).unwrap();
assert_eq!(binary.width, 10);
assert_eq!(binary.height, 10);
}
#[test]
fn test_all_algorithms_same_dimensions() {
let gray = create_uniform_image(20, 15, 128);
let none = apply_dithering(&gray, DitheringMethod::None).unwrap();
let floyd = apply_dithering(&gray, DitheringMethod::FloydSteinberg).unwrap();
let bayer = apply_dithering(&gray, DitheringMethod::Bayer).unwrap();
let atkinson = apply_dithering(&gray, DitheringMethod::Atkinson).unwrap();
assert_eq!(none.width, 20);
assert_eq!(none.height, 15);
assert_eq!(floyd.width, 20);
assert_eq!(floyd.height, 15);
assert_eq!(bayer.width, 20);
assert_eq!(bayer.height, 15);
assert_eq!(atkinson.width, 20);
assert_eq!(atkinson.height, 15);
}
#[test]
fn test_algorithms_produce_different_output() {
let gray = create_uniform_image(20, 20, 128);
let floyd = apply_dithering(&gray, DitheringMethod::FloydSteinberg).unwrap();
let bayer = apply_dithering(&gray, DitheringMethod::Bayer).unwrap();
let atkinson = apply_dithering(&gray, DitheringMethod::Atkinson).unwrap();
let all_same = floyd.pixels == bayer.pixels && floyd.pixels == atkinson.pixels;
assert!(
!all_same,
"Expected different patterns from different algorithms"
);
}
#[test]
fn test_large_image_performance_check() {
let gray = create_uniform_image(160, 96, 128);
let _ = floyd_steinberg(&gray).unwrap();
let _ = bayer(&gray).unwrap();
let _ = atkinson(&gray).unwrap();
}
#[test]
fn test_extreme_dimensions() {
let gray = create_uniform_image(1000, 1, 128);
assert!(floyd_steinberg(&gray).is_ok());
assert!(bayer(&gray).is_ok());
assert!(atkinson(&gray).is_ok());
let gray = create_uniform_image(1, 1000, 128);
assert!(floyd_steinberg(&gray).is_ok());
assert!(bayer(&gray).is_ok());
assert!(atkinson(&gray).is_ok());
}
}