#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::missing_docs_in_private_items)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::missing_const_for_fn)]
use image::{DynamicImage, GenericImageView, Rgb};
use tracing::debug;
use crate::image::{
adjust_brightness, adjust_contrast, adjust_gamma, apply_dithering,
apply_dithering_with_custom_threshold, apply_threshold, auto_threshold, pixels_to_braille,
to_grayscale, DitheringMethod,
};
use crate::{BrailleGrid, Color, DotmaxError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorMode {
Monochrome,
Grayscale,
TrueColor,
}
impl Default for ColorMode {
fn default() -> Self {
Self::Monochrome
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorSamplingStrategy {
Average,
Dominant,
CenterPixel,
}
impl Default for ColorSamplingStrategy {
fn default() -> Self {
Self::Average
}
}
pub fn extract_cell_colors(
image: &DynamicImage,
cell_width: usize,
cell_height: usize,
strategy: ColorSamplingStrategy,
) -> Vec<Color> {
let img_width = image.width() as usize;
let img_height = image.height() as usize;
let mut colors = Vec::with_capacity(cell_width * cell_height);
for cell_y in 0..cell_height {
for cell_x in 0..cell_width {
let px_start_x = cell_x * 2;
let px_start_y = cell_y * 4;
let mut block_pixels = Vec::with_capacity(8);
for py in 0..4 {
for px in 0..2 {
let x = px_start_x + px;
let y = px_start_y + py;
if x < img_width && y < img_height {
let pixel = image.get_pixel(x as u32, y as u32);
block_pixels.push(Rgb([pixel[0], pixel[1], pixel[2]]));
}
}
}
let cell_color = match strategy {
ColorSamplingStrategy::Average => average_color(&block_pixels),
ColorSamplingStrategy::Dominant => dominant_color(&block_pixels),
ColorSamplingStrategy::CenterPixel => center_pixel_color(&block_pixels),
};
colors.push(cell_color);
}
}
debug!(
"Extracted {} cell colors from {}×{} image using {:?} strategy",
colors.len(),
img_width,
img_height,
strategy
);
colors
}
pub fn average_color(pixels: &[Rgb<u8>]) -> Color {
if pixels.is_empty() {
return Color::rgb(0, 0, 0); }
let mut sum_r = 0u32;
let mut sum_g = 0u32;
let mut sum_b = 0u32;
for pixel in pixels {
sum_r += pixel[0] as u32;
sum_g += pixel[1] as u32;
sum_b += pixel[2] as u32;
}
let count = pixels.len() as u32;
Color::rgb(
(sum_r / count) as u8,
(sum_g / count) as u8,
(sum_b / count) as u8,
)
}
pub fn dominant_color(pixels: &[Rgb<u8>]) -> Color {
if pixels.is_empty() {
return Color::rgb(0, 0, 0);
}
let mut color_counts = std::collections::HashMap::new();
for pixel in pixels {
let color = Color::rgb(pixel[0], pixel[1], pixel[2]);
*color_counts.entry(color).or_insert(0) += 1;
}
color_counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map_or_else(|| Color::rgb(0, 0, 0), |(color, _)| color)
}
pub fn center_pixel_color(pixels: &[Rgb<u8>]) -> Color {
if pixels.is_empty() {
return Color::rgb(0, 0, 0);
}
let center_idx = pixels.len() / 2;
let pixel = pixels.get(center_idx).unwrap_or(&Rgb([0, 0, 0]));
Color::rgb(pixel[0], pixel[1], pixel[2])
}
pub fn rgb_to_grayscale_intensity(color: &Color) -> u8 {
let r = color.r as f32 * 0.2126;
let g = color.g as f32 * 0.7152;
let b = color.b as f32 * 0.0722;
(r + g + b).clamp(0.0, 255.0) as u8
}
pub fn intensity_to_ansi256(intensity: u8) -> u8 {
232 + ((intensity as u16 * 23) / 255) as u8
}
pub fn color_to_truecolor_ansi(color: &Color) -> String {
format!("\x1b[38;2;{};{};{}m", color.r, color.g, color.b)
}
#[allow(clippy::too_many_arguments)]
pub fn render_image_with_color(
image: &DynamicImage,
mode: ColorMode,
cell_width: usize,
cell_height: usize,
dithering: DitheringMethod,
threshold: Option<u8>,
brightness: f32,
contrast: f32,
gamma: f32,
) -> Result<BrailleGrid, DotmaxError> {
const EPSILON: f32 = 0.001;
let pixel_width = cell_width * 2; let pixel_height = cell_height * 4;
debug!(
"Rendering image with {:?} mode to {}×{} cells ({}×{} pixels)",
mode, cell_width, cell_height, pixel_width, pixel_height
);
let actual_pixel_width = image.width() as usize;
let actual_pixel_height = image.height() as usize;
let actual_cell_width = (actual_pixel_width + 1) / 2; let actual_cell_height = (actual_pixel_height + 3) / 4;
debug!(
"Image dimensions: {}×{} pixels → {}×{} cells",
actual_pixel_width, actual_pixel_height, actual_cell_width, actual_cell_height
);
let colors = if mode == ColorMode::Monochrome {
None
} else {
Some(extract_cell_colors(
image,
actual_cell_width,
actual_cell_height,
ColorSamplingStrategy::Average, ))
};
let mut gray = to_grayscale(image);
if (brightness - 1.0).abs() > EPSILON {
gray = adjust_brightness(&gray, brightness)?;
debug!("Applied brightness adjustment: {}", brightness);
}
if (contrast - 1.0).abs() > EPSILON {
gray = adjust_contrast(&gray, contrast)?;
debug!("Applied contrast adjustment: {}", contrast);
}
if (gamma - 1.0).abs() > EPSILON {
gray = adjust_gamma(&gray, gamma)?;
debug!("Applied gamma adjustment: {}", gamma);
}
let binary = if dithering == DitheringMethod::None {
if let Some(threshold_value) = threshold {
debug!("Applying manual threshold (no dithering): {}", threshold_value);
apply_threshold(&gray, threshold_value)
} else {
debug!("Applying automatic Otsu thresholding (no dithering)");
let gray_dynamic = DynamicImage::ImageLuma8(gray);
auto_threshold(&gray_dynamic)
}
} else {
if let Some(threshold_value) = threshold {
debug!(
"Applying {:?} dithering with manual threshold: {}",
dithering, threshold_value
);
apply_dithering_with_custom_threshold(&gray, dithering, Some(threshold_value))?
} else {
debug!(
"Applying {:?} dithering with default threshold (127)",
dithering
);
apply_dithering(&gray, dithering)?
}
};
let mut grid = pixels_to_braille(&binary, actual_cell_width, actual_cell_height)?;
if let Some(colors) = colors {
match mode {
ColorMode::Monochrome => {
}
ColorMode::Grayscale => {
for cell_y in 0..actual_cell_height {
for cell_x in 0..actual_cell_width {
let idx = cell_y * actual_cell_width + cell_x;
let color = &colors[idx];
let intensity = rgb_to_grayscale_intensity(color);
let gray_color = Color::rgb(intensity, intensity, intensity);
grid.set_cell_color(cell_x, cell_y, gray_color)?;
}
}
}
ColorMode::TrueColor => {
for cell_y in 0..actual_cell_height {
for cell_x in 0..actual_cell_width {
let idx = cell_y * actual_cell_width + cell_x;
grid.set_cell_color(cell_x, cell_y, colors[idx])?;
}
}
}
}
}
debug!(
"Rendered {}×{} grid with {} mode",
actual_cell_width,
actual_cell_height,
mode_name(mode)
);
Ok(grid)
}
const fn mode_name(mode: ColorMode) -> &'static str {
match mode {
ColorMode::Monochrome => "monochrome",
ColorMode::Grayscale => "grayscale",
ColorMode::TrueColor => "truecolor",
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::Rgb;
#[test]
fn test_color_mode_default() {
assert_eq!(ColorMode::default(), ColorMode::Monochrome);
}
#[test]
fn test_color_mode_derives() {
let mode1 = ColorMode::TrueColor;
let mode2 = ColorMode::TrueColor;
assert_eq!(mode1, mode2);
let mode3 = mode1;
assert_eq!(mode1, mode3);
}
#[test]
fn test_color_sampling_strategy_default() {
assert_eq!(
ColorSamplingStrategy::default(),
ColorSamplingStrategy::Average
);
}
#[test]
fn test_average_color_empty() {
let pixels: Vec<Rgb<u8>> = vec![];
let color = average_color(&pixels);
assert_eq!(color, Color::rgb(0, 0, 0));
}
#[test]
fn test_average_color_single() {
let pixels = vec![Rgb([255, 128, 64])];
let color = average_color(&pixels);
assert_eq!(color, Color::rgb(255, 128, 64));
}
#[test]
fn test_average_color_multiple() {
let pixels = vec![Rgb([255, 0, 0]), Rgb([0, 255, 0]), Rgb([0, 0, 255])];
let color = average_color(&pixels);
assert_eq!(color, Color::rgb(85, 85, 85));
}
#[test]
fn test_dominant_color_empty() {
let pixels: Vec<Rgb<u8>> = vec![];
let color = dominant_color(&pixels);
assert_eq!(color, Color::rgb(0, 0, 0));
}
#[test]
fn test_dominant_color_single() {
let pixels = vec![Rgb([255, 0, 0])];
let color = dominant_color(&pixels);
assert_eq!(color, Color::rgb(255, 0, 0));
}
#[test]
fn test_dominant_color_clear_winner() {
let pixels = vec![
Rgb([255, 0, 0]),
Rgb([255, 0, 0]),
Rgb([255, 0, 0]),
Rgb([255, 0, 0]),
Rgb([255, 0, 0]),
Rgb([255, 0, 0]), Rgb([0, 0, 255]),
Rgb([0, 0, 255]), ];
let color = dominant_color(&pixels);
assert_eq!(color, Color::rgb(255, 0, 0)); }
#[test]
fn test_center_pixel_color_empty() {
let pixels: Vec<Rgb<u8>> = vec![];
let color = center_pixel_color(&pixels);
assert_eq!(color, Color::rgb(0, 0, 0));
}
#[test]
fn test_center_pixel_color_single() {
let pixels = vec![Rgb([128, 64, 32])];
let color = center_pixel_color(&pixels);
assert_eq!(color, Color::rgb(128, 64, 32));
}
#[test]
fn test_center_pixel_color_full_block() {
let pixels = vec![
Rgb([0, 0, 0]),
Rgb([0, 0, 0]),
Rgb([0, 0, 0]),
Rgb([0, 0, 0]),
Rgb([255, 0, 0]), Rgb([0, 0, 0]),
Rgb([0, 0, 0]),
Rgb([0, 0, 0]),
];
let color = center_pixel_color(&pixels);
assert_eq!(color, Color::rgb(255, 0, 0));
}
#[test]
fn test_rgb_to_grayscale_intensity_black() {
let color = Color::rgb(0, 0, 0);
let intensity = rgb_to_grayscale_intensity(&color);
assert_eq!(intensity, 0);
}
#[test]
fn test_rgb_to_grayscale_intensity_white() {
let color = Color::rgb(255, 255, 255);
let intensity = rgb_to_grayscale_intensity(&color);
assert_eq!(intensity, 255);
}
#[test]
fn test_rgb_to_grayscale_intensity_red() {
let color = Color::rgb(255, 0, 0);
let intensity = rgb_to_grayscale_intensity(&color);
assert!((54..=55).contains(&intensity));
}
#[test]
fn test_rgb_to_grayscale_intensity_green() {
let color = Color::rgb(0, 255, 0);
let intensity = rgb_to_grayscale_intensity(&color);
assert!((182..=183).contains(&intensity));
}
#[test]
fn test_rgb_to_grayscale_intensity_blue() {
let color = Color::rgb(0, 0, 255);
let intensity = rgb_to_grayscale_intensity(&color);
assert!((18..=19).contains(&intensity));
}
#[test]
fn test_intensity_to_ansi256_black() {
let ansi = intensity_to_ansi256(0);
assert_eq!(ansi, 232); }
#[test]
fn test_intensity_to_ansi256_white() {
let ansi = intensity_to_ansi256(255);
assert_eq!(ansi, 255); }
#[test]
fn test_intensity_to_ansi256_mid() {
let ansi = intensity_to_ansi256(128);
assert!((243..=244).contains(&ansi));
}
#[test]
fn test_color_to_truecolor_ansi() {
let color = Color::rgb(255, 128, 64);
let ansi = color_to_truecolor_ansi(&color);
assert_eq!(ansi, "\x1b[38;2;255;128;64m");
}
#[test]
fn test_color_to_truecolor_ansi_black() {
let color = Color::rgb(0, 0, 0);
let ansi = color_to_truecolor_ansi(&color);
assert_eq!(ansi, "\x1b[38;2;0;0;0m");
}
#[test]
fn test_color_to_truecolor_ansi_white() {
let color = Color::rgb(255, 255, 255);
let ansi = color_to_truecolor_ansi(&color);
assert_eq!(ansi, "\x1b[38;2;255;255;255m");
}
}