use crate::error::DotmaxError;
use crate::image::loader::{MAX_IMAGE_HEIGHT, MAX_IMAGE_WIDTH};
use image::{imageops, DynamicImage};
use tracing::debug;
const BRAILLE_CELL_WIDTH: u16 = 2;
const BRAILLE_CELL_HEIGHT: u16 = 4;
const EXTREME_ASPECT_RATIO_THRESHOLD: f32 = 2.5;
pub fn resize_to_terminal(
image: &DynamicImage,
term_width: u16,
term_height: u16,
) -> Result<DynamicImage, DotmaxError> {
if term_width == 0 || term_height == 0 {
return Err(DotmaxError::InvalidImageDimensions {
width: u32::from(term_width),
height: u32::from(term_height),
});
}
let target_width_px = u32::from(term_width) * u32::from(BRAILLE_CELL_WIDTH);
let target_height_px = u32::from(term_height) * u32::from(BRAILLE_CELL_HEIGHT);
debug!(
"Resize to terminal: {}×{} cells → {}×{} pixels",
term_width, term_height, target_width_px, target_height_px
);
let src_width = image.width();
let src_height = image.height();
#[allow(clippy::cast_precision_loss)]
let aspect_ratio = src_width as f32 / src_height as f32;
debug!(
"Source image: {}×{}, aspect ratio: {:.2}",
src_width, src_height, aspect_ratio
);
let (final_width, final_height) =
calculate_fit_dimensions(src_width, src_height, target_width_px, target_height_px);
debug!(
"Final dimensions after aspect ratio preservation: {}×{}",
final_width, final_height
);
if final_width == src_width && final_height == src_height {
debug!("Image already at target size, skipping resize");
return Ok(image.clone());
}
let filter = select_resize_filter(src_width, src_height);
debug!(
"Resizing {}×{} → {}×{} using {:?} filter",
src_width, src_height, final_width, final_height, filter
);
let resized = imageops::resize(image, final_width, final_height, filter);
Ok(DynamicImage::ImageRgba8(resized))
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn calculate_fit_dimensions(src_w: u32, src_h: u32, target_w: u32, target_h: u32) -> (u32, u32) {
let src_aspect = src_w as f32 / src_h as f32;
let target_aspect = target_w as f32 / target_h as f32;
let (final_w, final_h) = if src_aspect > target_aspect {
let new_width = target_w;
let new_height = (target_w as f32 / src_aspect).round() as u32;
(new_width, new_height)
} else {
let new_height = target_h;
let new_width = (target_h as f32 * src_aspect).round() as u32;
(new_width, new_height)
};
let final_w = final_w.min(target_w).max(1); let final_h = final_h.min(target_h).max(1);
(final_w, final_h)
}
#[allow(clippy::cast_precision_loss)]
fn is_extreme_aspect_ratio(width: u32, height: u32) -> bool {
let aspect_ratio = width as f32 / height as f32;
aspect_ratio >= EXTREME_ASPECT_RATIO_THRESHOLD
|| aspect_ratio <= (1.0 / EXTREME_ASPECT_RATIO_THRESHOLD)
}
fn select_resize_filter(width: u32, height: u32) -> imageops::FilterType {
if is_extreme_aspect_ratio(width, height) {
debug!(
"Extreme aspect ratio detected ({}×{}), using Triangle filter for 3x faster performance",
width, height
);
imageops::FilterType::Triangle
} else {
imageops::FilterType::Lanczos3
}
}
pub fn resize_to_dimensions(
image: &DynamicImage,
target_width: u32,
target_height: u32,
preserve_aspect: bool,
) -> Result<DynamicImage, DotmaxError> {
if target_width == 0 || target_height == 0 {
return Err(DotmaxError::InvalidImageDimensions {
width: target_width,
height: target_height,
});
}
if target_width > MAX_IMAGE_WIDTH || target_height > MAX_IMAGE_HEIGHT {
return Err(DotmaxError::InvalidImageDimensions {
width: target_width,
height: target_height,
});
}
let src_width = image.width();
let src_height = image.height();
debug!(
"Resize to dimensions: {}×{} → {}×{}, preserve_aspect: {}",
src_width, src_height, target_width, target_height, preserve_aspect
);
let (final_width, final_height) = if preserve_aspect {
let dims = calculate_fit_dimensions(src_width, src_height, target_width, target_height);
debug!(
"Aspect ratio preserved: final dimensions {}×{}",
dims.0, dims.1
);
dims
} else {
debug!("Stretching to exact dimensions (aspect ratio not preserved)");
(target_width, target_height)
};
let filter = select_resize_filter(src_width, src_height);
debug!(
"Resizing {}×{} → {}×{} using {:?} filter (preserve_aspect: {})",
src_width, src_height, final_width, final_height, filter, preserve_aspect
);
let resized = imageops::resize(image, final_width, final_height, filter);
Ok(DynamicImage::ImageRgba8(resized))
}
#[cfg(test)]
mod tests {
use super::*;
use image::{DynamicImage, RgbaImage};
fn create_test_image(width: u32, height: u32) -> DynamicImage {
let img = RgbaImage::new(width, height);
DynamicImage::ImageRgba8(img)
}
#[test]
fn test_calculate_fit_dimensions_16_9() {
let (w, h) = calculate_fit_dimensions(1920, 1080, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
let aspect = w as f32 / h as f32;
let expected_aspect = 16.0 / 9.0;
assert!(
(aspect - expected_aspect).abs() < 0.01,
"Aspect ratio {:.3} != expected {:.3}",
aspect,
expected_aspect
);
}
#[test]
fn test_calculate_fit_dimensions_4_3() {
let (w, h) = calculate_fit_dimensions(800, 600, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
let aspect = w as f32 / h as f32;
let expected_aspect = 4.0 / 3.0;
assert!((aspect - expected_aspect).abs() < 0.01);
}
#[test]
fn test_calculate_fit_dimensions_1_1_square() {
let (w, h) = calculate_fit_dimensions(500, 500, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
assert_eq!(h, 96);
assert_eq!(w, 96);
}
#[test]
fn test_calculate_fit_dimensions_21_9_ultrawide() {
let (w, h) = calculate_fit_dimensions(2560, 1080, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
let aspect = w as f32 / h as f32;
let expected_aspect = 21.0 / 9.0;
assert!((aspect - expected_aspect).abs() < 0.1);
}
#[test]
fn test_letterboxing_wide_to_tall() {
let (w, h) = calculate_fit_dimensions(1000, 100, 200, 400);
assert_eq!(w, 200);
assert!(h < 400);
let aspect = w as f32 / h as f32;
let expected_aspect = 1000.0 / 100.0;
assert!((aspect - expected_aspect).abs() < 0.1);
}
#[test]
fn test_pillarboxing_tall_to_wide() {
let (w, h) = calculate_fit_dimensions(100, 1000, 400, 200);
assert_eq!(h, 200);
assert!(w < 400);
let aspect = w as f32 / h as f32;
let expected_aspect = 100.0 / 1000.0;
assert!((aspect - expected_aspect).abs() < 0.01);
}
#[test]
fn test_perfect_fit_same_aspect() {
let (w, h) = calculate_fit_dimensions(1920, 1080, 160, 90);
assert_eq!(w, 160);
assert_eq!(h, 90);
}
#[test]
fn test_resize_to_terminal_zero_width() {
let img = create_test_image(100, 100);
let result = resize_to_terminal(&img, 0, 24);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidImageDimensions { .. }
));
}
#[test]
fn test_resize_to_terminal_zero_height() {
let img = create_test_image(100, 100);
let result = resize_to_terminal(&img, 80, 0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidImageDimensions { .. }
));
}
#[test]
fn test_resize_to_dimensions_zero_width() {
let img = create_test_image(100, 100);
let result = resize_to_dimensions(&img, 0, 100, true);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidImageDimensions { .. }
));
}
#[test]
fn test_resize_to_dimensions_zero_height() {
let img = create_test_image(100, 100);
let result = resize_to_dimensions(&img, 100, 0, false);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidImageDimensions { .. }
));
}
#[test]
fn test_extreme_aspect_ratio_wide() {
let (w, h) = calculate_fit_dimensions(10000, 1, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
assert!(h >= 1); }
#[test]
fn test_extreme_aspect_ratio_tall() {
let (w, h) = calculate_fit_dimensions(1, 10000, 160, 96);
assert!(w <= 160);
assert!(h <= 96);
assert!(w >= 1); }
#[test]
fn test_resize_to_dimensions_preserve_aspect_true() {
let img = create_test_image(1920, 1080);
let result = resize_to_dimensions(&img, 200, 100, true);
assert!(result.is_ok());
let resized = result.unwrap();
assert!(resized.width() <= 200);
assert!(resized.height() <= 100);
}
#[test]
fn test_resize_to_dimensions_preserve_aspect_false() {
let img = create_test_image(1920, 1080);
let result = resize_to_dimensions(&img, 200, 100, false);
assert!(result.is_ok());
let resized = result.unwrap();
assert_eq!(resized.width(), 200);
assert_eq!(resized.height(), 100);
}
#[test]
fn test_resize_to_dimensions_exceeds_max_dimensions() {
let img = create_test_image(100, 100);
let result = resize_to_dimensions(&img, MAX_IMAGE_WIDTH + 1, 100, false);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidImageDimensions { .. }
));
}
#[test]
fn test_resize_to_terminal_basic() {
let img = create_test_image(800, 600);
let result = resize_to_terminal(&img, 80, 24);
assert!(result.is_ok());
let resized = result.unwrap();
assert!(resized.width() <= 160);
assert!(resized.height() <= 96);
}
#[test]
fn test_no_resize_when_already_correct_size() {
let img = create_test_image(160, 96);
let result = resize_to_terminal(&img, 80, 24);
assert!(result.is_ok());
let resized = result.unwrap();
assert_eq!(resized.width(), 160);
assert_eq!(resized.height(), 96);
}
#[test]
fn test_is_extreme_aspect_ratio_wide() {
assert!(is_extreme_aspect_ratio(10000, 100));
assert!(is_extreme_aspect_ratio(10000, 4000));
assert!(is_extreme_aspect_ratio(10000, 3000));
}
#[test]
fn test_is_extreme_aspect_ratio_tall() {
assert!(is_extreme_aspect_ratio(100, 10000));
assert!(is_extreme_aspect_ratio(4000, 10000));
assert!(is_extreme_aspect_ratio(3000, 10000));
}
#[test]
fn test_is_extreme_aspect_ratio_normal() {
assert!(!is_extreme_aspect_ratio(1920, 1080));
assert!(!is_extreme_aspect_ratio(800, 600));
assert!(!is_extreme_aspect_ratio(1000, 1000));
assert!(!is_extreme_aspect_ratio(2560, 1080));
}
#[test]
fn test_is_extreme_aspect_ratio_edge_cases() {
assert!(is_extreme_aspect_ratio(2500, 1000));
assert!(is_extreme_aspect_ratio(2501, 1000));
assert!(is_extreme_aspect_ratio(1000, 2500));
assert!(is_extreme_aspect_ratio(1000, 2501));
assert!(!is_extreme_aspect_ratio(2499, 1000));
assert!(!is_extreme_aspect_ratio(1000, 2499));
}
#[test]
fn test_select_resize_filter_normal() {
let filter = select_resize_filter(1920, 1080);
assert!(matches!(filter, imageops::FilterType::Lanczos3));
let filter = select_resize_filter(800, 600);
assert!(matches!(filter, imageops::FilterType::Lanczos3));
}
#[test]
fn test_select_resize_filter_extreme() {
let filter = select_resize_filter(10000, 100);
assert!(matches!(filter, imageops::FilterType::Triangle));
let filter = select_resize_filter(100, 10000);
assert!(matches!(filter, imageops::FilterType::Triangle));
}
}