Skip to main content

slimg_core/
resize.rs

1use image::imageops::FilterType;
2use image::{DynamicImage, RgbaImage};
3
4use crate::codec::ImageData;
5use crate::error::{Error, Result};
6
7/// How to resize an image.
8#[derive(Debug, Clone, PartialEq)]
9pub enum ResizeMode {
10    /// Set width, calculate height preserving aspect ratio.
11    Width(u32),
12    /// Set height, calculate width preserving aspect ratio.
13    Height(u32),
14    /// Exact dimensions (may distort the image).
15    Exact(u32, u32),
16    /// Fit within bounds, preserving aspect ratio.
17    Fit(u32, u32),
18    /// Scale factor (e.g. 0.5 = half size).
19    Scale(f64),
20}
21
22/// Calculate the target dimensions for a resize operation.
23pub 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
63/// Resize an image according to the given mode.
64pub 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}