Skip to main content

oar_ocr_core/utils/
crop.rs

1//! Image cropping functionality with different modes.
2
3use crate::core::errors::ImageProcessError;
4use crate::processors::types::CropMode;
5use crate::utils::image;
6use ::image::RgbImage;
7
8/// A processor for cropping images with different positioning modes.
9///
10/// The `Crop` struct provides functionality to crop images to a specified size
11/// using different positioning strategies (center, top-left, etc.).
12#[derive(Debug)]
13pub struct Crop {
14    /// The dimensions [width, height] for the crop operation.
15    crop_size: [u32; 2],
16    /// The mode determining how the crop region is positioned.
17    crop_mode: CropMode,
18}
19
20impl Crop {
21    /// Creates a new Crop instance with the specified parameters.
22    ///
23    /// # Arguments
24    ///
25    /// * `crop_size` - Array containing [width, height] for the crop operation.
26    /// * `crop_mode` - The positioning mode for the crop operation.
27    ///
28    /// # Returns
29    ///
30    /// * `Ok(Crop)` - A new Crop instance.
31    /// * `Err(ImageProcessError::InvalidCropSize)` - If either dimension is zero.
32    ///
33    /// # Examples
34    ///
35    /// ```rust,no_run
36    /// use oar_ocr_core::utils::crop::Crop;
37    /// use oar_ocr_core::processors::types::CropMode;
38    ///
39    /// # fn main() -> Result<(), oar_ocr_core::core::errors::ImageProcessError> {
40    /// let crop = Crop::new([224, 224], CropMode::Center)?;
41    /// # let _ = crop;
42    /// # Ok(())
43    /// # }
44    /// ```
45    pub fn new(crop_size: [u32; 2], crop_mode: CropMode) -> Result<Self, ImageProcessError> {
46        image::check_image_size(&crop_size)?;
47        Ok(Self {
48            crop_size,
49            crop_mode,
50        })
51    }
52
53    /// Gets the crop size.
54    ///
55    /// # Returns
56    ///
57    /// The crop size as [width, height].
58    pub fn crop_size(&self) -> [u32; 2] {
59        self.crop_size
60    }
61
62    /// Gets the crop mode.
63    ///
64    /// # Returns
65    ///
66    /// The crop mode.
67    pub fn crop_mode(&self) -> &CropMode {
68        &self.crop_mode
69    }
70
71    /// Processes an image by cropping it according to the configured parameters.
72    ///
73    /// # Arguments
74    ///
75    /// * `img` - Reference to the input image to be cropped.
76    ///
77    /// # Returns
78    ///
79    /// * `Ok(RgbImage)` - The cropped image.
80    /// * `Err(ImageProcessError)` - If the crop operation fails.
81    ///
82    /// # Examples
83    ///
84    /// ```rust,no_run
85    /// use image::RgbImage;
86    /// use oar_ocr_core::utils::crop::Crop;
87    /// use oar_ocr_core::processors::types::CropMode;
88    ///
89    /// # fn main() -> Result<(), oar_ocr_core::core::errors::ImageProcessError> {
90    /// let crop = Crop::new([100, 100], CropMode::Center)?;
91    /// let img = RgbImage::new(200, 200);
92    /// let cropped = crop.process(&img)?;
93    /// assert_eq!(cropped.dimensions(), (100, 100));
94    /// # Ok(())
95    /// # }
96    /// ```
97    pub fn process(&self, img: &RgbImage) -> Result<RgbImage, ImageProcessError> {
98        let (img_width, img_height) = img.dimensions();
99        let [crop_width, crop_height] = self.crop_size;
100
101        // Check if crop size is larger than image
102        if crop_width > img_width || crop_height > img_height {
103            return Err(ImageProcessError::CropSizeTooLarge);
104        }
105
106        // If the image is already the desired size, return a clone
107        if crop_width == img_width && crop_height == img_height {
108            return Ok(img.clone());
109        }
110
111        let (x, y) = self.calculate_crop_position(img_width, img_height)?;
112
113        // Validate the calculated position
114        image::validate_crop_bounds(img_width, img_height, x, y, crop_width, crop_height)?;
115
116        // Perform the crop
117        let coords = (x, y, x + crop_width, y + crop_height);
118        image::slice_image(img, coords)
119    }
120
121    /// Calculates the top-left position for the crop based on the crop mode.
122    ///
123    /// # Arguments
124    ///
125    /// * `img_width` - Width of the source image.
126    /// * `img_height` - Height of the source image.
127    ///
128    /// # Returns
129    ///
130    /// * `Ok((x, y))` - Top-left coordinates for the crop.
131    /// * `Err(ImageProcessError)` - If the calculation fails.
132    fn calculate_crop_position(
133        &self,
134        img_width: u32,
135        img_height: u32,
136    ) -> Result<(u32, u32), ImageProcessError> {
137        let [crop_width, crop_height] = self.crop_size;
138
139        match self.crop_mode {
140            CropMode::Center => {
141                image::calculate_center_crop_coords(img_width, img_height, crop_width, crop_height)
142            }
143            CropMode::TopLeft => Ok((0, 0)),
144            CropMode::TopRight => {
145                if crop_width > img_width {
146                    return Err(ImageProcessError::CropSizeTooLarge);
147                }
148                Ok((img_width - crop_width, 0))
149            }
150            CropMode::BottomLeft => {
151                if crop_height > img_height {
152                    return Err(ImageProcessError::CropSizeTooLarge);
153                }
154                Ok((0, img_height - crop_height))
155            }
156            CropMode::BottomRight => {
157                if crop_width > img_width || crop_height > img_height {
158                    return Err(ImageProcessError::CropSizeTooLarge);
159                }
160                Ok((img_width - crop_width, img_height - crop_height))
161            }
162            CropMode::Custom { x, y } => {
163                // Validate custom position
164                if x + crop_width > img_width || y + crop_height > img_height {
165                    return Err(ImageProcessError::CropOutOfBounds);
166                }
167                Ok((x, y))
168            }
169        }
170    }
171
172    /// Processes multiple images in batch.
173    ///
174    /// # Arguments
175    ///
176    /// * `images` - Vector of images to be cropped.
177    ///
178    /// # Returns
179    ///
180    /// * `Ok(Vec<RgbImage>)` - Vector of cropped images.
181    /// * `Err(ImageProcessError)` - If any crop operation fails.
182    pub fn process_batch(&self, images: &[RgbImage]) -> Result<Vec<RgbImage>, ImageProcessError> {
183        images.iter().map(|img| self.process(img)).collect()
184    }
185
186    /// Updates the crop size.
187    ///
188    /// # Arguments
189    ///
190    /// * `crop_size` - New crop size as [width, height].
191    ///
192    /// # Returns
193    ///
194    /// * `Ok(())` - If the size is valid.
195    /// * `Err(ImageProcessError::InvalidCropSize)` - If either dimension is zero.
196    pub fn set_crop_size(&mut self, crop_size: [u32; 2]) -> Result<(), ImageProcessError> {
197        image::check_image_size(&crop_size)?;
198        self.crop_size = crop_size;
199        Ok(())
200    }
201
202    /// Updates the crop mode.
203    ///
204    /// # Arguments
205    ///
206    /// * `crop_mode` - New crop mode.
207    pub fn set_crop_mode(&mut self, crop_mode: CropMode) {
208        self.crop_mode = crop_mode;
209    }
210
211    /// Checks if the crop can be applied to an image with the given dimensions.
212    ///
213    /// # Arguments
214    ///
215    /// * `img_width` - Width of the target image.
216    /// * `img_height` - Height of the target image.
217    ///
218    /// # Returns
219    ///
220    /// * `true` - If the crop can be applied.
221    /// * `false` - If the crop size is too large for the image.
222    pub fn can_crop(&self, img_width: u32, img_height: u32) -> bool {
223        let [crop_width, crop_height] = self.crop_size;
224        crop_width <= img_width && crop_height <= img_height
225    }
226
227    /// Gets the aspect ratio of the crop.
228    ///
229    /// # Returns
230    ///
231    /// The aspect ratio (width / height) of the crop.
232    pub fn aspect_ratio(&self) -> f32 {
233        let [crop_width, crop_height] = self.crop_size;
234        crop_width as f32 / crop_height as f32
235    }
236}
237
238impl Default for Crop {
239    /// Creates a default Crop instance with 224x224 center crop.
240    fn default() -> Self {
241        Self {
242            crop_size: [224, 224],
243            crop_mode: CropMode::Center,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use ::image::{Rgb, RgbImage};
252
253    fn create_test_image(width: u32, height: u32) -> RgbImage {
254        RgbImage::from_pixel(width, height, Rgb([255, 0, 0]))
255    }
256
257    #[test]
258    fn test_crop_center() -> Result<(), ImageProcessError> {
259        let crop = Crop::new([100, 100], CropMode::Center)?;
260        let img = create_test_image(200, 200);
261        let cropped = crop.process(&img)?;
262        assert_eq!(cropped.dimensions(), (100, 100));
263        Ok(())
264    }
265
266    #[test]
267    fn test_crop_top_left() -> Result<(), ImageProcessError> {
268        let crop = Crop::new([100, 100], CropMode::TopLeft)?;
269        let img = create_test_image(200, 200);
270        let cropped = crop.process(&img)?;
271        assert_eq!(cropped.dimensions(), (100, 100));
272        Ok(())
273    }
274
275    #[test]
276    fn test_crop_custom() -> Result<(), ImageProcessError> {
277        let crop = Crop::new([100, 100], CropMode::Custom { x: 50, y: 50 })?;
278        let img = create_test_image(200, 200);
279        let cropped = crop.process(&img)?;
280        assert_eq!(cropped.dimensions(), (100, 100));
281
282        // Test out of bounds custom position
283        let crop_oob = Crop::new([100, 100], CropMode::Custom { x: 150, y: 150 })?;
284        assert!(crop_oob.process(&img).is_err());
285        Ok(())
286    }
287
288    #[test]
289    fn test_crop_size_too_large() -> Result<(), ImageProcessError> {
290        let crop = Crop::new([300, 300], CropMode::Center)?;
291        let img = create_test_image(200, 200);
292        assert!(crop.process(&img).is_err());
293        Ok(())
294    }
295
296    #[test]
297    fn test_crop_same_size() -> Result<(), ImageProcessError> {
298        let crop = Crop::new([200, 200], CropMode::Center)?;
299        let img = create_test_image(200, 200);
300        let cropped = crop.process(&img)?;
301        assert_eq!(cropped.dimensions(), (200, 200));
302        Ok(())
303    }
304
305    #[test]
306    fn test_can_crop() -> Result<(), ImageProcessError> {
307        let crop = Crop::new([100, 100], CropMode::Center)?;
308        assert!(crop.can_crop(200, 200));
309        assert!(crop.can_crop(100, 100));
310        assert!(!crop.can_crop(50, 50));
311        Ok(())
312    }
313
314    #[test]
315    fn test_aspect_ratio() -> Result<(), ImageProcessError> {
316        let crop = Crop::new([200, 100], CropMode::Center)?;
317        assert_eq!(crop.aspect_ratio(), 2.0);
318        Ok(())
319    }
320
321    #[test]
322    fn test_process_batch() -> Result<(), ImageProcessError> {
323        let crop = Crop::new([100, 100], CropMode::Center)?;
324        let images = vec![create_test_image(200, 200), create_test_image(300, 300)];
325        let cropped = crop.process_batch(&images)?;
326        assert_eq!(cropped.len(), 2);
327        assert_eq!(cropped[0].dimensions(), (100, 100));
328        assert_eq!(cropped[1].dimensions(), (100, 100));
329        Ok(())
330    }
331}