1use image::imageops::FilterType;
2use image::{DynamicImage, RgbaImage};
3
4use crate::codec::ImageData;
5use crate::error::{Error, Result};
6
7#[derive(Debug, Clone, PartialEq)]
9pub enum ResizeMode {
10 Width(u32),
12 Height(u32),
14 Exact(u32, u32),
16 Fit(u32, u32),
18 Scale(f64),
20}
21
22pub fn calculate_dimensions(orig_w: u32, orig_h: u32, mode: &ResizeMode) -> Result<(u32, u32)> {
24 let (w, h) = match *mode {
25 ResizeMode::Width(target_w) => {
26 let ratio = target_w as f64 / orig_w as f64;
27 let target_h = (orig_h as f64 * ratio).round() as u32;
28 (target_w, target_h)
29 }
30 ResizeMode::Height(target_h) => {
31 let ratio = target_h as f64 / orig_h as f64;
32 let target_w = (orig_w as f64 * ratio).round() as u32;
33 (target_w, target_h)
34 }
35 ResizeMode::Exact(w, h) => (w, h),
36 ResizeMode::Fit(max_w, max_h) => {
37 let ratio_w = max_w as f64 / orig_w as f64;
38 let ratio_h = max_h as f64 / orig_h as f64;
39 let ratio = ratio_w.min(ratio_h);
40 let target_w = (orig_w as f64 * ratio).round() as u32;
41 let target_h = (orig_h as f64 * ratio).round() as u32;
42 (target_w, target_h)
43 }
44 ResizeMode::Scale(factor) => {
45 if factor <= 0.0 {
46 return Err(Error::Resize("scale factor must be positive".to_string()));
47 }
48 let target_w = (orig_w as f64 * factor).round() as u32;
49 let target_h = (orig_h as f64 * factor).round() as u32;
50 (target_w, target_h)
51 }
52 };
53
54 if w == 0 || h == 0 {
55 return Err(Error::Resize(format!(
56 "calculated dimensions are zero: {w}x{h}"
57 )));
58 }
59
60 Ok((w, h))
61}
62
63pub fn resize(image: &ImageData, mode: &ResizeMode) -> Result<ImageData> {
65 let (target_w, target_h) = calculate_dimensions(image.width, image.height, mode)?;
66
67 let rgba =
68 RgbaImage::from_raw(image.width, image.height, image.data.clone()).ok_or_else(|| {
69 Error::Resize(format!(
70 "failed to create RgbaImage from {}x{} data ({} bytes)",
71 image.width,
72 image.height,
73 image.data.len(),
74 ))
75 })?;
76
77 let dynamic = DynamicImage::ImageRgba8(rgba);
78
79 let resized = match mode {
80 ResizeMode::Exact(_, _) => dynamic.resize_exact(target_w, target_h, FilterType::Lanczos3),
81 _ => dynamic.resize(target_w, target_h, FilterType::Lanczos3),
82 };
83
84 let output = resized.to_rgba8();
85 Ok(ImageData::new(
86 output.width(),
87 output.height(),
88 output.into_raw(),
89 ))
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn create_test_image(width: u32, height: u32) -> ImageData {
97 let data = vec![128u8; (width * height * 4) as usize];
98 ImageData::new(width, height, data)
99 }
100
101 #[test]
102 fn resize_by_width_preserves_ratio() {
103 let img = create_test_image(200, 100);
104 let result = resize(&img, &ResizeMode::Width(100)).unwrap();
105 assert_eq!(result.width, 100);
106 assert_eq!(result.height, 50);
107 }
108
109 #[test]
110 fn resize_by_height_preserves_ratio() {
111 let img = create_test_image(200, 100);
112 let result = resize(&img, &ResizeMode::Height(50)).unwrap();
113 assert_eq!(result.width, 100);
114 assert_eq!(result.height, 50);
115 }
116
117 #[test]
118 fn resize_fit_within_bounds() {
119 let img = create_test_image(400, 200);
120 let result = resize(&img, &ResizeMode::Fit(100, 100)).unwrap();
121 assert_eq!(result.width, 100);
122 assert_eq!(result.height, 50);
123 }
124
125 #[test]
126 fn resize_by_scale() {
127 let img = create_test_image(200, 100);
128 let result = resize(&img, &ResizeMode::Scale(0.5)).unwrap();
129 assert_eq!(result.width, 100);
130 assert_eq!(result.height, 50);
131 }
132
133 #[test]
134 fn resize_exact_ignores_ratio() {
135 let img = create_test_image(200, 100);
136 let result = resize(&img, &ResizeMode::Exact(50, 50)).unwrap();
137 assert_eq!(result.width, 50);
138 assert_eq!(result.height, 50);
139 }
140}