1pub mod enhanced;
16
17use chrono::{DateTime, Utc};
18use image::AnimationDecoder;
19use scirs2_core::ndarray::Array3;
20use std::fs;
21use std::io::BufReader;
22use std::path::Path;
23
24use crate::error::{IoError, Result};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ColorMode {
29 Grayscale,
31 RGB,
33 RGBA,
35 CMYK,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ImageFormat {
42 PNG,
44 JPEG,
46 BMP,
48 TIFF,
50 GIF,
52 WEBP,
54 Other,
56}
57
58impl ImageFormat {
59 pub fn from_extension(ext: &str) -> Self {
61 match ext.to_lowercase().as_str() {
62 "png" => ImageFormat::PNG,
63 "jpg" | "jpeg" => ImageFormat::JPEG,
64 "bmp" => ImageFormat::BMP,
65 "tiff" | "tif" => ImageFormat::TIFF,
66 "gif" => ImageFormat::GIF,
67 "webp" => ImageFormat::WEBP,
68 _ => ImageFormat::Other,
69 }
70 }
71
72 pub fn extension(&self) -> &'static str {
74 match self {
75 ImageFormat::PNG => "png",
76 ImageFormat::JPEG => "jpg",
77 ImageFormat::BMP => "bmp",
78 ImageFormat::TIFF => "tiff",
79 ImageFormat::GIF => "gif",
80 ImageFormat::WEBP => "webp",
81 ImageFormat::Other => "unknown",
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct ImageMetadata {
89 pub width: u32,
91 pub height: u32,
93 pub color_mode: ColorMode,
95 pub format: ImageFormat,
97 pub file_size: u64,
99 pub exif: Option<ExifMetadata>,
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct GpsCoordinates {
106 pub latitude: Option<f64>,
108 pub longitude: Option<f64>,
110 pub altitude: Option<f64>,
112}
113
114#[derive(Debug, Clone, Default)]
116pub struct CameraSettings {
117 pub make: Option<String>,
119 pub model: Option<String>,
121 pub lens_model: Option<String>,
123 pub iso: Option<u32>,
125 pub aperture: Option<f64>,
127 pub shutter_speed: Option<f64>,
129 pub focal_length: Option<f64>,
131 pub flash: Option<bool>,
133 pub white_balance: Option<String>,
135 pub exposure_mode: Option<String>,
137 pub metering_mode: Option<String>,
139}
140
141#[derive(Debug, Clone, Default)]
143pub struct ExifMetadata {
144 pub datetime: Option<DateTime<Utc>>,
146 pub gps: Option<GpsCoordinates>,
148 pub camera: CameraSettings,
150 pub orientation: Option<u32>,
152 pub software: Option<String>,
154 pub copyright: Option<String>,
156 pub artist: Option<String>,
158 pub description: Option<String>,
160 pub raw_tags: std::collections::HashMap<String, String>,
162}
163
164#[derive(Debug, Clone)]
166pub struct ImageData {
167 pub data: Array3<u8>,
169 pub metadata: ImageMetadata,
171}
172
173#[derive(Debug, Clone)]
175pub struct AnimationData {
176 pub frames: Vec<ImageData>,
178 pub delays: Vec<u32>,
180 pub loop_count: u32,
182}
183
184#[allow(dead_code)]
201pub fn load_image<P: AsRef<Path>>(path: P) -> Result<ImageData> {
202 let _path = path.as_ref();
203 let img = image::open(_path).map_err(|e| IoError::FileError(e.to_string()))?;
204
205 let width = img.width();
206 let height = img.height();
207 let format =
208 ImageFormat::from_extension(_path.extension().and_then(|ext| ext.to_str()).unwrap_or(""));
209
210 let file_size = fs::metadata(_path)
211 .map(|metadata| metadata.len())
212 .unwrap_or(0);
213
214 let rgb_img = img.to_rgb8();
216 let raw_data = rgb_img.into_raw();
217
218 let data = Array3::from_shape_vec((height as usize, width as usize, 3), raw_data)
220 .map_err(|e| IoError::FormatError(e.to_string()))?;
221
222 let exif = read_exif_metadata(_path)?;
224
225 let metadata = ImageMetadata {
226 width,
227 height,
228 color_mode: ColorMode::RGB,
229 format,
230 file_size,
231 exif,
232 };
233
234 Ok(ImageData { data, metadata })
235}
236
237#[allow(dead_code)]
253pub fn save_image<P: AsRef<Path>>(
254 image_data: &ImageData,
255 path: P,
256 format: Option<ImageFormat>,
257) -> Result<()> {
258 let path = path.as_ref();
259 let format = format.unwrap_or_else(|| {
260 ImageFormat::from_extension(path.extension().and_then(|ext| ext.to_str()).unwrap_or(""))
261 });
262
263 let (height, width_, _) = image_data.data.dim();
264 let raw_data = image_data.data.iter().cloned().collect::<Vec<u8>>();
265
266 let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
267 .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
268
269 let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
270
271 match format {
272 ImageFormat::PNG => dynamic_img.save_with_format(path, image::ImageFormat::Png),
273 ImageFormat::JPEG => dynamic_img.save_with_format(path, image::ImageFormat::Jpeg),
274 ImageFormat::BMP => dynamic_img.save_with_format(path, image::ImageFormat::Bmp),
275 ImageFormat::TIFF => dynamic_img.save_with_format(path, image::ImageFormat::Tiff),
276 ImageFormat::GIF => dynamic_img.save_with_format(path, image::ImageFormat::Gif),
277 ImageFormat::WEBP => dynamic_img.save_with_format(path, image::ImageFormat::WebP),
278 ImageFormat::Other => return Err(IoError::FormatError("Unknown format".to_string())),
279 }
280 .map_err(|e| IoError::FileError(e.to_string()))?;
281
282 Ok(())
283}
284
285#[allow(dead_code)]
300pub fn convert_image<P1: AsRef<Path>, P2: AsRef<Path>>(
301 input_path: P1,
302 output_path: P2,
303 target_format: ImageFormat,
304) -> Result<()> {
305 let image_data = load_image(input_path)?;
306 save_image(&image_data, output_path, Some(target_format))
307}
308
309#[allow(dead_code)]
328pub fn resize_image(image_data: &ImageData, new_width: u32, new_height: u32) -> Result<ImageData> {
329 let (_height, width_, _) = image_data.data.dim();
330 let raw_data = image_data.data.iter().cloned().collect::<Vec<u8>>();
331
332 let img_buffer = image::RgbImage::from_raw(width_ as u32, _height as u32, raw_data)
333 .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
334
335 let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
336 let resized_img =
337 dynamic_img.resize(new_width, new_height, image::imageops::FilterType::Lanczos3);
338 let rgb_img = resized_img.to_rgb8();
339 let resized_raw = rgb_img.into_raw();
340
341 let resized_data =
342 Array3::from_shape_vec((new_height as usize, new_width as usize, 3), resized_raw)
343 .map_err(|e| IoError::FormatError(e.to_string()))?;
344
345 let mut new_metadata = image_data.metadata.clone();
346 new_metadata.width = new_width;
347 new_metadata.height = new_height;
348
349 Ok(ImageData {
350 data: resized_data,
351 metadata: new_metadata,
352 })
353}
354
355#[allow(dead_code)]
372pub fn get_image_info<P: AsRef<Path>>(path: P) -> Result<ImageMetadata> {
373 let _path = path.as_ref();
374 let reader = image::ImageReader::open(_path).map_err(|e| IoError::FileError(e.to_string()))?;
375
376 let reader = reader
377 .with_guessed_format()
378 .map_err(|e| IoError::FileError(e.to_string()))?;
379
380 let dimensions = reader
381 .into_dimensions()
382 .map_err(|e| IoError::FileError(e.to_string()))?;
383
384 let format =
385 ImageFormat::from_extension(_path.extension().and_then(|ext| ext.to_str()).unwrap_or(""));
386
387 let file_size = fs::metadata(_path)
388 .map(|metadata| metadata.len())
389 .unwrap_or(0);
390
391 let exif = read_exif_metadata(_path)?;
393
394 Ok(ImageMetadata {
395 width: dimensions.0,
396 height: dimensions.1,
397 color_mode: ColorMode::RGB, format,
399 file_size,
400 exif,
401 })
402}
403
404#[allow(dead_code)]
421pub fn load_animation<P: AsRef<Path>>(path: P) -> Result<AnimationData> {
422 let _path = path.as_ref();
423 let file = std::fs::File::open(_path).map_err(|e| IoError::FileError(e.to_string()))?;
424 let reader = BufReader::new(file);
425
426 let decoder = image::codecs::gif::GifDecoder::new(reader)
427 .map_err(|e| IoError::FileError(e.to_string()))?;
428
429 let mut frames = Vec::new();
430 let mut delays = Vec::new();
431
432 for frame_result in decoder.into_frames() {
433 let frame = frame_result.map_err(|e| IoError::FileError(e.to_string()))?;
434 let delay = frame.delay().numer_denom_ms().0;
435 let image = frame.into_buffer();
436
437 let width = image.width();
438 let height = image.height();
439 let raw_data = image.into_raw();
440
441 let rgb_data = if raw_data.len() == (width * height * 4) as usize {
443 raw_data
445 .chunks(4)
446 .flat_map(|rgba| &rgba[..3])
447 .cloned()
448 .collect()
449 } else {
450 raw_data
451 };
452
453 let data = Array3::from_shape_vec((height as usize, width as usize, 3), rgb_data)
454 .map_err(|e| IoError::FormatError(e.to_string()))?;
455
456 let metadata = ImageMetadata {
457 width,
458 height,
459 color_mode: ColorMode::RGB,
460 format: ImageFormat::GIF,
461 file_size: 0, exif: None,
463 };
464
465 frames.push(ImageData { data, metadata });
466 delays.push(delay);
467 }
468
469 Ok(AnimationData {
470 frames,
471 delays,
472 loop_count: 0, })
474}
475
476#[allow(dead_code)]
496pub fn read_exif_metadata<P: AsRef<Path>>(path: P) -> Result<Option<ExifMetadata>> {
497 let _path = path.as_ref();
498
499 #[cfg(feature = "exif")]
501 {
502 use std::fs::File;
503 use std::io::BufReader;
504
505 let file = match File::open(_path) {
506 Ok(f) => f,
507 Err(_) => return Ok(None), };
509
510 let mut reader = BufReader::new(file);
511
512 let exif_reader = match exif::Reader::new().read_from_container(&mut reader) {
513 Ok(reader) => reader,
514 Err(_) => return Ok(None), };
516
517 let mut metadata = ExifMetadata::default();
518
519 if let Some(field) = exif_reader.get_field(exif::Tag::DateTime, exif::In::PRIMARY) {
521 if let exif::Value::Ascii(ref vec) = field.value {
522 if let Some(ascii_str) = vec.first() {
523 if let Ok(datetime_str) = std::str::from_utf8(ascii_str) {
524 if let Ok(datetime) = chrono::NaiveDateTime::parse_from_str(
526 datetime_str.trim_end_matches('\0'),
527 "%Y:%m:%d %H:%M:%S",
528 ) {
529 metadata.datetime = Some(datetime.and_utc());
530 }
531 }
532 }
533 }
534 }
535
536 let mut gps = GpsCoordinates::default();
538
539 if let Some(lat_field) = exif_reader.get_field(exif::Tag::GPSLatitude, exif::In::PRIMARY) {
541 if let Some(lat_ref_field) =
542 exif_reader.get_field(exif::Tag::GPSLatitudeRef, exif::In::PRIMARY)
543 {
544 if let (exif::Value::Rational(ref lat_vec), exif::Value::Ascii(ref lat_ref_vec)) =
545 (&lat_field.value, &lat_ref_field.value)
546 {
547 if lat_vec.len() >= 3 && !lat_ref_vec.is_empty() {
548 let degrees = lat_vec[0].to_f64();
549 let minutes = lat_vec[1].to_f64();
550 let seconds = lat_vec[2].to_f64();
551
552 let mut latitude = degrees + minutes / 60.0 + seconds / 3600.0;
553
554 if let Ok(ref_str) = std::str::from_utf8(&lat_ref_vec[0]) {
556 if ref_str.starts_with('S') {
557 latitude = -latitude;
558 }
559 }
560
561 gps.latitude = Some(latitude);
562 }
563 }
564 }
565 }
566
567 if let Some(lon_field) = exif_reader.get_field(exif::Tag::GPSLongitude, exif::In::PRIMARY) {
569 if let Some(lon_ref_field) =
570 exif_reader.get_field(exif::Tag::GPSLongitudeRef, exif::In::PRIMARY)
571 {
572 if let (exif::Value::Rational(ref lon_vec), exif::Value::Ascii(ref lon_ref_vec)) =
573 (&lon_field.value, &lon_ref_field.value)
574 {
575 if lon_vec.len() >= 3 && !lon_ref_vec.is_empty() {
576 let degrees = lon_vec[0].to_f64();
577 let minutes = lon_vec[1].to_f64();
578 let seconds = lon_vec[2].to_f64();
579
580 let mut longitude = degrees + minutes / 60.0 + seconds / 3600.0;
581
582 if let Ok(ref_str) = std::str::from_utf8(&lon_ref_vec[0]) {
584 if ref_str.starts_with('W') {
585 longitude = -longitude;
586 }
587 }
588
589 gps.longitude = Some(longitude);
590 }
591 }
592 }
593 }
594
595 if let Some(alt_field) = exif_reader.get_field(exif::Tag::GPSAltitude, exif::In::PRIMARY) {
597 if let exif::Value::Rational(ref alt_vec) = alt_field.value {
598 if !alt_vec.is_empty() {
599 gps.altitude = Some(alt_vec[0].to_f64());
600 }
601 }
602 }
603
604 if gps.latitude.is_some() || gps.longitude.is_some() || gps.altitude.is_some() {
605 metadata.gps = Some(gps);
606 }
607
608 let mut camera = CameraSettings::default();
610
611 if let Some(field) = exif_reader.get_field(exif::Tag::Make, exif::In::PRIMARY) {
613 if let exif::Value::Ascii(ref vec) = field.value {
614 if let Some(ascii_str) = vec.first() {
615 if let Ok(make_str) = std::str::from_utf8(ascii_str) {
616 camera.make = Some(make_str.trim_end_matches('\0').to_string());
617 }
618 }
619 }
620 }
621
622 if let Some(field) = exif_reader.get_field(exif::Tag::Model, exif::In::PRIMARY) {
624 if let exif::Value::Ascii(ref vec) = field.value {
625 if let Some(ascii_str) = vec.first() {
626 if let Ok(model_str) = std::str::from_utf8(ascii_str) {
627 camera.model = Some(model_str.trim_end_matches('\0').to_string());
628 }
629 }
630 }
631 }
632
633 if let Some(field) = exif_reader.get_field(exif::Tag::LensModel, exif::In::PRIMARY) {
635 if let exif::Value::Ascii(ref vec) = field.value {
636 if let Some(ascii_str) = vec.first() {
637 if let Ok(lens_str) = std::str::from_utf8(ascii_str) {
638 camera.lens_model = Some(lens_str.trim_end_matches('\0').to_string());
639 }
640 }
641 }
642 }
643
644 if let Some(field) =
646 exif_reader.get_field(exif::Tag::PhotographicSensitivity, exif::In::PRIMARY)
647 {
648 if let exif::Value::Short(ref vec) = field.value {
649 if !vec.is_empty() {
650 camera.iso = Some(vec[0] as u32);
651 }
652 }
653 }
654
655 if let Some(field) = exif_reader.get_field(exif::Tag::FNumber, exif::In::PRIMARY) {
657 if let exif::Value::Rational(ref vec) = field.value {
658 if !vec.is_empty() {
659 camera.aperture = Some(vec[0].to_f64());
660 }
661 }
662 }
663
664 if let Some(field) = exif_reader.get_field(exif::Tag::ExposureTime, exif::In::PRIMARY) {
666 if let exif::Value::Rational(ref vec) = field.value {
667 if !vec.is_empty() {
668 camera.shutter_speed = Some(vec[0].to_f64());
669 }
670 }
671 }
672
673 if let Some(field) = exif_reader.get_field(exif::Tag::FocalLength, exif::In::PRIMARY) {
675 if let exif::Value::Rational(ref vec) = field.value {
676 if !vec.is_empty() {
677 camera.focal_length = Some(vec[0].to_f64());
678 }
679 }
680 }
681
682 if let Some(field) = exif_reader.get_field(exif::Tag::Flash, exif::In::PRIMARY) {
684 if let exif::Value::Short(ref vec) = field.value {
685 if !vec.is_empty() {
686 camera.flash = Some((vec[0] & 1) == 1); }
688 }
689 }
690
691 if let Some(field) = exif_reader.get_field(exif::Tag::WhiteBalance, exif::In::PRIMARY) {
693 if let exif::Value::Short(ref vec) = field.value {
694 if !vec.is_empty() {
695 camera.white_balance = Some(match vec[0] {
696 0 => "Auto".to_string(),
697 1 => "Manual".to_string(),
698 _ => "Unknown".to_string(),
699 });
700 }
701 }
702 }
703
704 metadata.camera = camera;
705
706 if let Some(field) = exif_reader.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
708 if let exif::Value::Short(ref vec) = field.value {
709 if !vec.is_empty() {
710 metadata.orientation = Some(vec[0] as u32);
711 }
712 }
713 }
714
715 if let Some(field) = exif_reader.get_field(exif::Tag::Software, exif::In::PRIMARY) {
717 if let exif::Value::Ascii(ref vec) = field.value {
718 if let Some(ascii_str) = vec.first() {
719 if let Ok(software_str) = std::str::from_utf8(ascii_str) {
720 metadata.software = Some(software_str.trim_end_matches('\0').to_string());
721 }
722 }
723 }
724 }
725
726 if let Some(field) = exif_reader.get_field(exif::Tag::Copyright, exif::In::PRIMARY) {
728 if let exif::Value::Ascii(ref vec) = field.value {
729 if let Some(ascii_str) = vec.first() {
730 if let Ok(copyright_str) = std::str::from_utf8(ascii_str) {
731 metadata.copyright = Some(copyright_str.trim_end_matches('\0').to_string());
732 }
733 }
734 }
735 }
736
737 if let Some(field) = exif_reader.get_field(exif::Tag::Artist, exif::In::PRIMARY) {
739 if let exif::Value::Ascii(ref vec) = field.value {
740 if let Some(ascii_str) = vec.first() {
741 if let Ok(artist_str) = std::str::from_utf8(ascii_str) {
742 metadata.artist = Some(artist_str.trim_end_matches('\0').to_string());
743 }
744 }
745 }
746 }
747
748 if let Some(field) = exif_reader.get_field(exif::Tag::ImageDescription, exif::In::PRIMARY) {
750 if let exif::Value::Ascii(ref vec) = field.value {
751 if let Some(ascii_str) = vec.first() {
752 if let Ok(desc_str) = std::str::from_utf8(ascii_str) {
753 metadata.description = Some(desc_str.trim_end_matches('\0').to_string());
754 }
755 }
756 }
757 }
758
759 for field in exif_reader.fields() {
761 let tag_name = format!("{}", field.tag);
762 let value_str = format!("{}", field.display_value().with_unit(&exif_reader));
763 metadata.raw_tags.insert(tag_name, value_str);
764 }
765
766 Ok(Some(metadata))
767 }
768
769 #[cfg(not(feature = "exif"))]
770 {
771 Ok(None)
773 }
774}
775
776#[allow(dead_code)]
797pub fn find_images<P: AsRef<Path>>(
798 dir_path: P,
799 pattern: &str,
800 recursive: bool,
801) -> Result<Vec<std::path::PathBuf>> {
802 let mut paths: Vec<std::path::PathBuf> = Vec::new();
803 collect_matching_files(dir_path.as_ref(), pattern, recursive, &mut paths);
804
805 let mut paths: Vec<std::path::PathBuf> = paths
806 .into_iter()
807 .filter(|path| {
808 let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
809 matches!(
810 ext.to_lowercase().as_str(),
811 "jpg" | "jpeg" | "png" | "bmp" | "tiff" | "tif" | "gif" | "webp"
812 )
813 })
814 .collect();
815
816 paths.sort();
818 Ok(paths)
819}
820
821fn collect_matching_files(
825 dir: &Path,
826 pattern: &str,
827 recursive: bool,
828 out: &mut Vec<std::path::PathBuf>,
829) {
830 let entries = match std::fs::read_dir(dir) {
831 Ok(entries) => entries,
832 Err(_) => return,
833 };
834 for entry in entries.flatten() {
835 let path = entry.path();
836 if path.is_dir() {
837 if recursive {
838 collect_matching_files(&path, pattern, recursive, out);
839 }
840 } else if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
841 if glob_match_file_name(pattern, name) {
842 out.push(path);
843 }
844 }
845 }
846}
847
848fn glob_match_file_name(pattern: &str, name: &str) -> bool {
852 let pat: Vec<char> = pattern.chars().collect();
853 let txt: Vec<char> = name.chars().collect();
854 let (mut p, mut t) = (0usize, 0usize);
855 let (mut star_p, mut star_t): (Option<usize>, usize) = (None, 0);
857
858 while t < txt.len() {
859 let matched = match pat.get(p) {
860 Some('*') => {
861 star_p = Some(p);
862 star_t = t;
863 p += 1;
864 continue;
865 }
866 Some('?') => true,
867 Some('[') => match match_char_class(&pat, p, txt[t]) {
868 Some((hit, next_p)) => {
869 if hit {
870 p = next_p;
871 t += 1;
872 continue;
873 }
874 false
875 }
876 None => pat.get(p) == Some(&txt[t]),
879 },
880 Some(&c) => c == txt[t],
881 None => false,
882 };
883
884 if matched {
885 p += 1;
886 t += 1;
887 } else if let Some(sp) = star_p {
888 star_t += 1;
890 t = star_t;
891 p = sp + 1;
892 } else {
893 return false;
894 }
895 }
896 pat[p..].iter().all(|&c| c == '*')
898}
899
900fn match_char_class(pat: &[char], start: usize, c: char) -> Option<(bool, usize)> {
904 let mut i = start + 1;
905 let negated = matches!(pat.get(i), Some('!') | Some('^'));
906 if negated {
907 i += 1;
908 }
909 let mut matched = false;
910 let mut first = true;
911 while let Some(&pc) = pat.get(i) {
912 if pc == ']' && !first {
913 return Some((matched != negated, i + 1));
914 }
915 first = false;
916 if let (Some(&'-'), Some(&hi)) = (pat.get(i + 1), pat.get(i + 2)) {
918 if hi != ']' {
919 if pc <= c && c <= hi {
920 matched = true;
921 }
922 i += 3;
923 continue;
924 }
925 }
926 if pc == c {
927 matched = true;
928 }
929 i += 1;
930 }
931 None
932}
933
934#[allow(dead_code)]
953pub fn batch_process_images<P1, P2, F>(input_dir: P1, output_dir: P2, processor: F) -> Result<()>
954where
955 P1: AsRef<Path>,
956 P2: AsRef<Path>,
957 F: Fn(&ImageData) -> Result<ImageData>,
958{
959 let input_dir = input_dir.as_ref();
960 let output_dir = output_dir.as_ref();
961
962 fs::create_dir_all(output_dir).map_err(|e| IoError::FileError(e.to_string()))?;
964
965 let image_files = find_images(input_dir, "*", false)?;
966
967 for input_path in image_files {
968 let file_name = input_path
969 .file_name()
970 .ok_or_else(|| IoError::FileError("Invalid file name".to_string()))?;
971 let output_path = output_dir.join(file_name);
972
973 let image_data = load_image(&input_path)?;
974 let processed_data = processor(&image_data)?;
975 save_image(&processed_data, &output_path, None)?;
976
977 println!(
978 "Processed: {} -> {}",
979 input_path.display(),
980 output_path.display()
981 );
982 }
983
984 Ok(())
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
992 fn test_glob_match_file_name() {
993 assert!(glob_match_file_name("*.jpg", "photo.jpg"));
995 assert!(glob_match_file_name("*", "anything.png"));
996 assert!(!glob_match_file_name("*.jpg", "photo.png"));
997 assert!(glob_match_file_name("img?.png", "img1.png"));
999 assert!(!glob_match_file_name("img?.png", "img10.png"));
1000 assert!(glob_match_file_name("img[0-9].png", "img5.png"));
1002 assert!(!glob_match_file_name("img[0-9].png", "imgx.png"));
1003 assert!(glob_match_file_name("img[!0-9].png", "imgx.png"));
1004 assert!(glob_match_file_name("exact.bmp", "exact.bmp"));
1006 assert!(!glob_match_file_name("*.JPG", "photo.jpg"));
1007 assert!(glob_match_file_name("a*b*c", "axxbyyc"));
1009 assert!(!glob_match_file_name("a*b*c", "axxbyy"));
1010 }
1011
1012 #[test]
1013 fn test_find_images_sorted_and_filtered() -> Result<()> {
1014 let dir =
1015 std::env::temp_dir().join(format!("scirs2_io_find_images_test_{}", std::process::id()));
1016 let sub = dir.join("sub");
1017 std::fs::create_dir_all(&sub).map_err(|e| IoError::FileError(e.to_string()))?;
1018 for name in ["b.jpg", "a.jpg", "c.txt"] {
1019 std::fs::write(dir.join(name), b"x").map_err(|e| IoError::FileError(e.to_string()))?;
1020 }
1021 std::fs::write(sub.join("d.jpg"), b"x").map_err(|e| IoError::FileError(e.to_string()))?;
1022
1023 let non_recursive = find_images(&dir, "*.jpg", false)?;
1024 let names: Vec<String> = non_recursive
1025 .iter()
1026 .filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(String::from))
1027 .collect();
1028 assert_eq!(names, ["a.jpg", "b.jpg"], "sorted, non-recursive");
1029
1030 let recursive = find_images(&dir, "*.jpg", true)?;
1031 assert_eq!(recursive.len(), 3, "recursive picks up sub/d.jpg");
1032
1033 std::fs::remove_dir_all(&dir).map_err(|e| IoError::FileError(e.to_string()))?;
1034 Ok(())
1035 }
1036
1037 #[test]
1038 fn test_image_format_from_extension() {
1039 assert_eq!(ImageFormat::from_extension("jpg"), ImageFormat::JPEG);
1040 assert_eq!(ImageFormat::from_extension("png"), ImageFormat::PNG);
1041 assert_eq!(ImageFormat::from_extension("unknown"), ImageFormat::Other);
1042 }
1043
1044 #[test]
1045 fn test_image_format_extension() {
1046 assert_eq!(ImageFormat::JPEG.extension(), "jpg");
1047 assert_eq!(ImageFormat::PNG.extension(), "png");
1048 }
1049
1050 #[test]
1051 fn test_color_mode() {
1052 let grayscale = ColorMode::Grayscale;
1053 let rgb = ColorMode::RGB;
1054 assert_ne!(grayscale, rgb);
1055 }
1056
1057 #[test]
1058 fn test_metadata_creation() {
1059 let metadata = ImageMetadata {
1060 width: 800,
1061 height: 600,
1062 color_mode: ColorMode::RGB,
1063 format: ImageFormat::JPEG,
1064 file_size: 1024,
1065 exif: None,
1066 };
1067
1068 assert_eq!(metadata.width, 800);
1069 assert_eq!(metadata.height, 600);
1070 }
1071}