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