1use crate::core::OCRError;
9use crate::core::errors::ImageProcessError;
10use image::{DynamicImage, GrayImage, ImageBuffer, ImageError, ImageReader, RgbImage};
11use std::fs::File;
12use std::io::BufReader;
13use std::path::Path;
14
15pub fn dynamic_to_rgb(img: DynamicImage) -> RgbImage {
28 img.to_rgb8()
29}
30
31pub fn dynamic_to_gray(img: DynamicImage) -> GrayImage {
44 img.to_luma8()
45}
46
47pub fn load_image_from_memory(bytes: &[u8]) -> Result<RgbImage, OCRError> {
66 let img = image::load_from_memory(bytes).map_err(OCRError::ImageLoad)?;
67 Ok(dynamic_to_rgb(img))
68}
69
70pub fn load_image<P: AsRef<Path>>(path: P) -> Result<RgbImage, OCRError> {
89 let img = open_image_any_format(path.as_ref()).map_err(OCRError::ImageLoad)?;
90 Ok(dynamic_to_rgb(img))
91}
92
93fn open_image_any_format(path: &Path) -> Result<DynamicImage, ImageError> {
94 let file = File::open(path)?;
95 let reader = BufReader::new(file);
96 let reader = ImageReader::new(reader).with_guessed_format()?;
97 reader.decode()
98}
99
100pub fn create_rgb_image(width: u32, height: u32, data: Vec<u8>) -> Option<RgbImage> {
117 if data.len() != (width * height * 3) as usize {
118 return None;
119 }
120
121 ImageBuffer::from_raw(width, height, data)
122}
123
124pub fn check_image_size(size: &[u32; 2]) -> Result<(), ImageProcessError> {
126 if size[0] == 0 || size[1] == 0 {
127 return Err(ImageProcessError::InvalidCropSize);
128 }
129 Ok(())
130}
131
132pub fn slice_image(
134 img: &RgbImage,
135 coords: (u32, u32, u32, u32),
136) -> Result<RgbImage, ImageProcessError> {
137 let (x1, y1, x2, y2) = coords;
138 let (img_width, img_height) = img.dimensions();
139
140 if x1 >= x2 || y1 >= y2 {
141 return Err(ImageProcessError::InvalidCropCoordinates);
142 }
143
144 if x2 > img_width || y2 > img_height {
145 return Err(ImageProcessError::CropOutOfBounds);
146 }
147
148 let crop_width = x2 - x1;
149 let crop_height = y2 - y1;
150
151 Ok(image::imageops::crop_imm(img, x1, y1, crop_width, crop_height).to_image())
152}
153
154pub fn slice_gray_image(
156 img: &GrayImage,
157 coords: (u32, u32, u32, u32),
158) -> Result<GrayImage, ImageProcessError> {
159 let (x1, y1, x2, y2) = coords;
160 let (img_width, img_height) = img.dimensions();
161
162 if x1 >= x2 || y1 >= y2 {
163 return Err(ImageProcessError::InvalidCropCoordinates);
164 }
165
166 if x2 > img_width || y2 > img_height {
167 return Err(ImageProcessError::CropOutOfBounds);
168 }
169
170 let crop_width = x2 - x1;
171 let crop_height = y2 - y1;
172
173 Ok(image::imageops::crop_imm(img, x1, y1, crop_width, crop_height).to_image())
174}
175
176pub fn calculate_center_crop_coords(
178 img_width: u32,
179 img_height: u32,
180 crop_width: u32,
181 crop_height: u32,
182) -> Result<(u32, u32), ImageProcessError> {
183 if crop_width > img_width || crop_height > img_height {
184 return Err(ImageProcessError::CropSizeTooLarge);
185 }
186
187 let x = (img_width - crop_width) / 2;
188 let y = (img_height - crop_height) / 2;
189
190 Ok((x, y))
191}
192
193pub fn validate_crop_bounds(
195 img_width: u32,
196 img_height: u32,
197 x: u32,
198 y: u32,
199 crop_width: u32,
200 crop_height: u32,
201) -> Result<(), ImageProcessError> {
202 if x + crop_width > img_width || y + crop_height > img_height {
203 return Err(ImageProcessError::CropOutOfBounds);
204 }
205 Ok(())
206}
207
208pub fn resize_image(
214 img: &RgbImage,
215 width: u32,
216 height: u32,
217) -> Result<RgbImage, ImageProcessError> {
218 if width == 0 || height == 0 {
219 return Err(ImageProcessError::InvalidCropSize);
220 }
221 Ok(image::imageops::resize(
222 img,
223 width,
224 height,
225 image::imageops::FilterType::Lanczos3,
226 ))
227}
228
229pub fn resize_gray_image(
235 img: &GrayImage,
236 width: u32,
237 height: u32,
238) -> Result<GrayImage, ImageProcessError> {
239 if width == 0 || height == 0 {
240 return Err(ImageProcessError::InvalidCropSize);
241 }
242 Ok(image::imageops::resize(
243 img,
244 width,
245 height,
246 image::imageops::FilterType::Lanczos3,
247 ))
248}
249
250pub fn rgb_to_grayscale(img: &RgbImage) -> GrayImage {
252 image::imageops::grayscale(img)
253}
254
255pub fn pad_image(
257 img: &RgbImage,
258 target_width: u32,
259 target_height: u32,
260 fill_color: [u8; 3],
261) -> Result<RgbImage, ImageProcessError> {
262 let (src_width, src_height) = img.dimensions();
263
264 if target_width < src_width || target_height < src_height {
265 return Err(ImageProcessError::InvalidCropSize);
266 }
267
268 if target_width == src_width && target_height == src_height {
269 return Ok(img.clone());
270 }
271
272 let mut padded = RgbImage::from_pixel(target_width, target_height, image::Rgb(fill_color));
273 let x_offset = (target_width - src_width) / 2;
274 let y_offset = (target_height - src_height) / 2;
275 image::imageops::overlay(&mut padded, img, x_offset as i64, y_offset as i64);
276
277 Ok(padded)
278}
279
280pub fn load_images<P: AsRef<std::path::Path> + Send + Sync>(
300 paths: &[P],
301) -> Result<Vec<RgbImage>, OCRError> {
302 load_images_batch_with_threshold(paths, None)
303}
304
305pub fn load_images_batch_with_threshold<P: AsRef<std::path::Path> + Send + Sync>(
328 paths: &[P],
329 parallel_threshold: Option<usize>,
330) -> Result<Vec<RgbImage>, OCRError> {
331 use crate::core::constants::DEFAULT_PARALLEL_THRESHOLD;
332
333 let threshold = parallel_threshold.unwrap_or(DEFAULT_PARALLEL_THRESHOLD);
334
335 if paths.len() > threshold {
336 use rayon::prelude::*;
337 paths.par_iter().map(|p| load_image(p.as_ref())).collect()
338 } else {
339 paths.iter().map(|p| load_image(p.as_ref())).collect()
340 }
341}
342
343pub fn load_images_batch_with_policy<P: AsRef<std::path::Path> + Send + Sync>(
364 paths: &[P],
365 policy: &crate::core::config::ParallelPolicy,
366) -> Result<Vec<RgbImage>, OCRError> {
367 if paths.len() > policy.utility_threshold {
368 use rayon::prelude::*;
369 paths.par_iter().map(|p| load_image(p.as_ref())).collect()
370 } else {
371 paths.iter().map(|p| load_image(p.as_ref())).collect()
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Default)]
377pub enum PaddingStrategy {
378 SolidColor([u8; 3]),
380 #[default]
382 Black,
383 LeftAlign([u8; 3]),
385}
386
387#[derive(Debug, Clone)]
389pub struct ResizePadConfig {
390 pub target_dims: (u32, u32),
392 pub padding_strategy: PaddingStrategy,
394 pub filter_type: image::imageops::FilterType,
396}
397
398impl ResizePadConfig {
399 pub fn new(target_dims: (u32, u32)) -> Self {
401 Self {
402 target_dims,
403 padding_strategy: PaddingStrategy::default(),
404 filter_type: image::imageops::FilterType::Triangle,
405 }
406 }
407
408 pub fn with_padding_strategy(mut self, strategy: PaddingStrategy) -> Self {
410 self.padding_strategy = strategy;
411 self
412 }
413
414 pub fn with_filter_type(mut self, filter_type: image::imageops::FilterType) -> Self {
416 self.filter_type = filter_type;
417 self
418 }
419}
420
421pub fn resize_and_pad(
440 image: &RgbImage,
441 config: &ResizePadConfig,
442) -> Result<RgbImage, ImageProcessError> {
443 let (target_width, target_height) = config.target_dims;
444
445 if target_width == 0 || target_height == 0 {
446 return Err(ImageProcessError::InvalidCropSize);
447 }
448
449 let (orig_width, orig_height) = image.dimensions();
450
451 let scale_w = target_width as f32 / orig_width as f32;
453 let scale_h = target_height as f32 / orig_height as f32;
454 let scale = scale_w.min(scale_h);
455
456 let new_width = (orig_width as f32 * scale) as u32;
458 let new_height = (orig_height as f32 * scale) as u32;
459
460 let resized = image::imageops::resize(image, new_width, new_height, config.filter_type);
462
463 let padding_color = match config.padding_strategy {
465 PaddingStrategy::SolidColor(color) => color,
466 PaddingStrategy::Black => [0, 0, 0],
467 PaddingStrategy::LeftAlign(color) => color,
468 };
469 let padding_rgb = image::Rgb(padding_color);
470 let mut padded = ImageBuffer::from_pixel(target_width, target_height, padding_rgb);
471
472 let (pad_x, pad_y) = match config.padding_strategy {
474 PaddingStrategy::LeftAlign(_) => (0, 0),
475 _ => {
476 let pad_x = (target_width - new_width) / 2;
478 let pad_y = (target_height - new_height) / 2;
479 (pad_x, pad_y)
480 }
481 };
482
483 image::imageops::overlay(&mut padded, &resized, pad_x as i64, pad_y as i64);
485
486 Ok(padded)
487}
488
489#[derive(Debug, Clone)]
491pub struct OCRResizePadConfig {
492 pub target_height: u32,
494 pub max_width: u32,
496 pub padding_strategy: PaddingStrategy,
498 pub filter_type: image::imageops::FilterType,
500}
501
502impl OCRResizePadConfig {
503 pub fn new(target_height: u32, max_width: u32) -> Self {
507 Self {
508 target_height,
509 max_width,
510 padding_strategy: PaddingStrategy::default(),
511 filter_type: image::imageops::FilterType::Triangle,
513 }
514 }
515
516 pub fn with_padding_strategy(mut self, strategy: PaddingStrategy) -> Self {
518 self.padding_strategy = strategy;
519 self
520 }
521
522 pub fn with_filter_type(mut self, filter_type: image::imageops::FilterType) -> Self {
524 self.filter_type = filter_type;
525 self
526 }
527}
528
529pub fn ocr_resize_and_pad(
552 image: &RgbImage,
553 config: &OCRResizePadConfig,
554 target_width_ratio: Option<f32>,
555) -> Result<(RgbImage, u32), ImageProcessError> {
556 if config.target_height == 0 {
557 return Err(ImageProcessError::InvalidCropSize);
558 }
559
560 let (original_w, original_h) = image.dimensions();
561 let original_ratio = original_w as f32 / original_h as f32;
562
563 let mut target_w = if let Some(ratio) = target_width_ratio {
565 (config.target_height as f32 * ratio) as u32
566 } else {
567 (config.target_height as f32 * original_ratio).ceil() as u32
568 };
569
570 let resized_w = if target_w > config.max_width {
572 target_w = config.max_width;
573 config.max_width
574 } else {
575 let ratio = original_w as f32 / original_h as f32;
577 if (config.target_height as f32 * ratio).ceil() as u32 > target_w {
578 target_w
579 } else {
580 (config.target_height as f32 * ratio).ceil() as u32
581 }
582 };
583
584 let resized_image =
586 image::imageops::resize(image, resized_w, config.target_height, config.filter_type);
587
588 let padding_color = match config.padding_strategy {
590 PaddingStrategy::SolidColor(color) => color,
591 PaddingStrategy::Black => [0, 0, 0],
592 PaddingStrategy::LeftAlign(color) => color,
593 };
594 let padding_rgb = image::Rgb(padding_color);
595 let mut padded_image = ImageBuffer::from_pixel(target_w, config.target_height, padding_rgb);
596
597 image::imageops::overlay(&mut padded_image, &resized_image, 0, 0);
599
600 Ok((padded_image, target_w))
601}
602
603pub fn resize_images_batch(
632 images: &[RgbImage],
633 target_width: u32,
634 target_height: u32,
635 filter_type: Option<image::imageops::FilterType>,
636) -> Vec<RgbImage> {
637 let filter = filter_type.unwrap_or(image::imageops::FilterType::Lanczos3);
638
639 images
640 .iter()
641 .map(|img| image::imageops::resize(img, target_width, target_height, filter))
642 .collect()
643}
644
645pub fn resize_images_batch_to_dynamic(
661 images: &[RgbImage],
662 target_width: u32,
663 target_height: u32,
664 filter_type: Option<image::imageops::FilterType>,
665) -> Vec<DynamicImage> {
666 let filter = filter_type.unwrap_or(image::imageops::FilterType::Lanczos3);
667
668 images
669 .iter()
670 .map(|img| {
671 let resized = image::imageops::resize(img, target_width, target_height, filter);
672 DynamicImage::ImageRgb8(resized)
673 })
674 .collect()
675}
676
677pub fn mask_region(
710 image: &mut RgbImage,
711 x1: u32,
712 y1: u32,
713 x2: u32,
714 y2: u32,
715 fill_color: [u8; 3],
716) -> Result<(), ImageProcessError> {
717 let (img_width, img_height) = image.dimensions();
718
719 let x1 = x1.min(img_width);
721 let y1 = y1.min(img_height);
722 let x2 = x2.min(img_width);
723 let y2 = y2.min(img_height);
724
725 if x1 >= x2 || y1 >= y2 {
726 return Err(ImageProcessError::InvalidCropCoordinates);
727 }
728
729 let rgb = image::Rgb(fill_color);
730 for y in y1..y2 {
731 for x in x1..x2 {
732 image.put_pixel(x, y, rgb);
733 }
734 }
735
736 Ok(())
737}
738
739pub fn mask_regions(
767 image: &mut RgbImage,
768 bboxes: &[crate::processors::BoundingBox],
769 fill_color: [u8; 3],
770) {
771 for bbox in bboxes {
772 let x1 = bbox.x_min() as u32;
773 let y1 = bbox.y_min() as u32;
774 let x2 = bbox.x_max() as u32;
775 let y2 = bbox.y_max() as u32;
776
777 let _ = mask_region(image, x1, y1, x2, y2, fill_color);
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use ::image::{GenericImageView, GrayImage, ImageBuffer, Rgb, RgbImage};
786
787 fn create_test_image(width: u32, height: u32, color: [u8; 3]) -> RgbImage {
788 ImageBuffer::from_pixel(width, height, Rgb(color))
789 }
790
791 #[test]
792 fn basic_size_checks() {
793 assert!(check_image_size(&[100, 100]).is_ok());
794 assert!(check_image_size(&[0, 50]).is_err());
795 }
796
797 #[test]
798 fn slice_rgb_image_region() -> Result<(), ImageProcessError> {
799 let img = RgbImage::from_pixel(10, 10, Rgb([255, 0, 0]));
800 let cropped = slice_image(&img, (2, 2, 6, 6))?;
801 assert_eq!(cropped.dimensions(), (4, 4));
802 assert!(slice_image(&img, (6, 6, 2, 2)).is_err());
803 Ok(())
804 }
805
806 #[test]
807 fn slice_gray_image_region() -> Result<(), ImageProcessError> {
808 let img = GrayImage::from_pixel(10, 10, image::Luma([128]));
809 let cropped = slice_gray_image(&img, (1, 1, 5, 5))?;
810 assert_eq!(cropped.dimensions(), (4, 4));
811 Ok(())
812 }
813
814 #[test]
815 fn center_crop_coordinates() -> Result<(), ImageProcessError> {
816 let coords = calculate_center_crop_coords(100, 60, 40, 20)?;
817 assert_eq!(coords, (30, 20));
818 assert!(calculate_center_crop_coords(20, 20, 40, 10).is_err());
819 Ok(())
820 }
821
822 #[test]
823 fn crop_bounds_validation() {
824 assert!(validate_crop_bounds(100, 80, 10, 10, 40, 40).is_ok());
825 assert!(validate_crop_bounds(100, 80, 70, 10, 40, 40).is_err());
826 }
827
828 #[test]
829 fn pad_image_to_target() -> Result<(), ImageProcessError> {
830 let img = RgbImage::from_pixel(20, 20, Rgb([10, 20, 30]));
831 let padded = pad_image(&img, 40, 40, [0, 0, 0])?;
832 assert_eq!(padded.dimensions(), (40, 40));
833 assert!(pad_image(&img, 10, 10, [0, 0, 0]).is_err());
834 Ok(())
835 }
836
837 #[test]
838 fn test_resize_and_pad_with_custom_padding() -> Result<(), ImageProcessError> {
839 let image = create_test_image(50, 100, [255, 0, 0]); let config = ResizePadConfig::new((80, 80))
841 .with_padding_strategy(PaddingStrategy::SolidColor([0, 255, 0])); let result = resize_and_pad(&image, &config)?;
844
845 assert_eq!(result.dimensions(), (80, 80));
846
847 let center_pixel = result.get_pixel(40, 40); assert_eq!(*center_pixel, Rgb([255, 0, 0])); let left_padding = result.get_pixel(10, 40); assert_eq!(*left_padding, Rgb([0, 255, 0])); Ok(())
855 }
856
857 #[test]
858 fn test_resize_and_pad_left_align() -> Result<(), ImageProcessError> {
859 let image = create_test_image(50, 100, [0, 0, 255]); let config = ResizePadConfig::new((80, 80))
861 .with_padding_strategy(PaddingStrategy::LeftAlign([255, 255, 0])); let result = resize_and_pad(&image, &config)?;
864
865 assert_eq!(result.dimensions(), (80, 80));
866
867 let left_edge_pixel = result.get_pixel(20, 40); assert_eq!(*left_edge_pixel, Rgb([0, 0, 255])); let right_padding = result.get_pixel(60, 40); assert_eq!(*right_padding, Rgb([255, 255, 0])); Ok(())
874 }
875
876 #[test]
877 fn test_resize_images_batch() {
878 let img1 = create_test_image(100, 50, [255, 0, 0]); let img2 = create_test_image(200, 100, [0, 255, 0]); let images = vec![img1, img2];
882
883 let resized = resize_images_batch(&images, 64, 64, None);
885
886 assert_eq!(resized.len(), 2);
887 assert_eq!(resized[0].dimensions(), (64, 64));
888 assert_eq!(resized[1].dimensions(), (64, 64));
889
890 let pixel1 = resized[0].get_pixel(32, 32);
892 let pixel2 = resized[1].get_pixel(32, 32);
893
894 assert!(pixel1[0] > pixel1[1] && pixel1[0] > pixel1[2]);
896 assert!(pixel2[1] > pixel2[0] && pixel2[1] > pixel2[2]);
898 }
899
900 #[test]
901 fn test_resize_images_batch_to_dynamic() {
902 let img1 = create_test_image(100, 50, [255, 0, 0]);
904 let img2 = create_test_image(200, 100, [0, 255, 0]);
905 let images = vec![img1, img2];
906
907 let resized = resize_images_batch_to_dynamic(&images, 32, 32, None);
909
910 assert_eq!(resized.len(), 2);
911
912 for dynamic_img in &resized {
914 assert_eq!(dynamic_img.dimensions(), (32, 32));
915 assert!(
916 matches!(dynamic_img, DynamicImage::ImageRgb8(_)),
917 "Expected ImageRgb8 variant"
918 );
919 }
920 }
921
922 #[test]
923 fn test_resize_images_batch_empty() {
924 let images: Vec<RgbImage> = vec![];
925 let resized = resize_images_batch(&images, 64, 64, None);
926 assert!(resized.is_empty());
927 }
928
929 #[test]
930 fn test_resize_images_batch_custom_filter() {
931 let img = create_test_image(100, 100, [128, 128, 128]);
932 let images = vec![img];
933
934 let resized_lanczos =
936 resize_images_batch(&images, 50, 50, Some(image::imageops::FilterType::Lanczos3));
937 let resized_nearest =
938 resize_images_batch(&images, 50, 50, Some(image::imageops::FilterType::Nearest));
939
940 assert_eq!(resized_lanczos.len(), 1);
941 assert_eq!(resized_nearest.len(), 1);
942 assert_eq!(resized_lanczos[0].dimensions(), (50, 50));
943 assert_eq!(resized_nearest[0].dimensions(), (50, 50));
944 }
945
946 #[test]
947 fn test_ocr_resize_and_pad_with_max_width_constraint() -> Result<(), ImageProcessError> {
948 let image = create_test_image(400, 100, [200, 100, 50]); let config = OCRResizePadConfig::new(32, 100); let (result, actual_width) = ocr_resize_and_pad(&image, &config, None)?;
952
953 assert_eq!(result.height(), 32);
954 assert_eq!(actual_width, 100); assert_eq!(result.width(), 100);
956
957 let left_pixel = result.get_pixel(0, 16); assert_eq!(*left_pixel, Rgb([200, 100, 50])); Ok(())
961 }
962
963 #[test]
964 fn test_ocr_resize_and_pad_with_target_ratio() -> Result<(), ImageProcessError> {
965 let image = create_test_image(100, 50, [255, 128, 64]); let config = OCRResizePadConfig::new(32, 200); let target_ratio = 3.0; let (result, actual_width) = ocr_resize_and_pad(&image, &config, Some(target_ratio))?;
970
971 assert_eq!(result.height(), 32);
972 assert_eq!(actual_width, 96); assert_eq!(result.width(), 96);
974 Ok(())
975 }
976
977 #[test]
978 fn test_resize_pad_config_builder() {
979 let config = ResizePadConfig::new((100, 50))
980 .with_padding_strategy(PaddingStrategy::SolidColor([255, 0, 0]))
981 .with_filter_type(image::imageops::FilterType::Lanczos3);
982
983 assert_eq!(config.target_dims, (100, 50));
984 assert_eq!(
985 config.padding_strategy,
986 PaddingStrategy::SolidColor([255, 0, 0])
987 );
988 assert_eq!(config.filter_type, image::imageops::FilterType::Lanczos3);
989 }
990
991 #[test]
992 fn test_ocr_resize_pad_config_builder() {
993 let config = OCRResizePadConfig::new(64, 320)
994 .with_padding_strategy(PaddingStrategy::SolidColor([100, 100, 100]))
995 .with_filter_type(image::imageops::FilterType::Nearest);
996
997 assert_eq!(config.target_height, 64);
998 assert_eq!(config.max_width, 320);
999 assert_eq!(
1000 config.padding_strategy,
1001 PaddingStrategy::SolidColor([100, 100, 100])
1002 );
1003 assert_eq!(config.filter_type, image::imageops::FilterType::Nearest);
1004 }
1005}