Skip to main content

scirs2_io/image/
mod.rs

1//! Image file format support
2//!
3//! This module provides functionality for reading and writing common image formats.
4//! It supports basic operations like loading, saving, and converting between formats.
5//!
6//! Features:
7//! - Reading and writing common image formats (PNG, JPEG, BMP, TIFF)
8//! - Basic EXIF metadata extraction (currently limited)
9//! - Conversion between different image formats
10//! - Basic image properties and information
11//! - Image sequence handling and animations (GIF, sequence of images)
12//! - Enhanced capabilities: multi-scale pyramids, lossless compression, advanced processing
13
14/// Enhanced image capabilities with multi-scale support and lossless compression
15pub 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/// Image color mode
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ColorMode {
29    /// Grayscale (single channel)
30    Grayscale,
31    /// RGB color (3 channels)
32    RGB,
33    /// RGBA color with alpha (4 channels)
34    RGBA,
35    /// CMYK color (4 channels)
36    CMYK,
37}
38
39/// Image format enumeration
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ImageFormat {
42    /// PNG format
43    PNG,
44    /// JPEG format
45    JPEG,
46    /// BMP format
47    BMP,
48    /// TIFF format
49    TIFF,
50    /// GIF format
51    GIF,
52    /// WebP format
53    WEBP,
54    /// Other/Unknown format
55    Other,
56}
57
58impl ImageFormat {
59    /// Get format from file extension
60    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    /// Get file extension for format
73    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/// Basic image metadata
87#[derive(Debug, Clone)]
88pub struct ImageMetadata {
89    /// Image width in pixels
90    pub width: u32,
91    /// Image height in pixels
92    pub height: u32,
93    /// Color mode/channels
94    pub color_mode: ColorMode,
95    /// File format
96    pub format: ImageFormat,
97    /// File size in bytes
98    pub file_size: u64,
99    /// EXIF metadata (if available)
100    pub exif: Option<ExifMetadata>,
101}
102
103/// GPS coordinates extracted from EXIF
104#[derive(Debug, Clone, Default)]
105pub struct GpsCoordinates {
106    /// Latitude in decimal degrees
107    pub latitude: Option<f64>,
108    /// Longitude in decimal degrees  
109    pub longitude: Option<f64>,
110    /// Altitude in meters
111    pub altitude: Option<f64>,
112}
113
114/// Camera settings from EXIF
115#[derive(Debug, Clone, Default)]
116pub struct CameraSettings {
117    /// Camera make
118    pub make: Option<String>,
119    /// Camera model
120    pub model: Option<String>,
121    /// Lens model
122    pub lens_model: Option<String>,
123    /// ISO sensitivity
124    pub iso: Option<u32>,
125    /// Aperture (f-number)
126    pub aperture: Option<f64>,
127    /// Shutter speed
128    pub shutter_speed: Option<f64>,
129    /// Focal length in mm
130    pub focal_length: Option<f64>,
131    /// Flash fired
132    pub flash: Option<bool>,
133    /// White balance setting
134    pub white_balance: Option<String>,
135    /// Exposure mode
136    pub exposure_mode: Option<String>,
137    /// Metering mode
138    pub metering_mode: Option<String>,
139}
140
141/// EXIF metadata
142#[derive(Debug, Clone, Default)]
143pub struct ExifMetadata {
144    /// Date and time photo was taken
145    pub datetime: Option<DateTime<Utc>>,
146    /// GPS coordinates
147    pub gps: Option<GpsCoordinates>,
148    /// Camera settings
149    pub camera: CameraSettings,
150    /// Image orientation
151    pub orientation: Option<u32>,
152    /// Software used
153    pub software: Option<String>,
154    /// Copyright information
155    pub copyright: Option<String>,
156    /// Artist/photographer
157    pub artist: Option<String>,
158    /// Image description
159    pub description: Option<String>,
160    /// Raw EXIF tags
161    pub raw_tags: std::collections::HashMap<String, String>,
162}
163
164/// Image data container
165#[derive(Debug, Clone)]
166pub struct ImageData {
167    /// Image data as ndarray
168    pub data: Array3<u8>,
169    /// Image metadata
170    pub metadata: ImageMetadata,
171}
172
173/// Animation/sequence data
174#[derive(Debug, Clone)]
175pub struct AnimationData {
176    /// Frames as Vec of ImageData
177    pub frames: Vec<ImageData>,
178    /// Frame delays in milliseconds
179    pub delays: Vec<u32>,
180    /// Loop count (0 = infinite)
181    pub loop_count: u32,
182}
183
184/// Load image from file
185///
186/// # Arguments
187/// * `path` - Path to image file
188///
189/// # Returns
190/// Result containing ImageData with pixel data and metadata
191///
192/// # Example
193/// ```no_run
194/// use scirs2_io::image::load_image;
195///
196/// let image_data = load_image("photo.jpg")?;
197/// println!("Image size: {}x{}", image_data.metadata.width, image_data.metadata.height);
198/// # Ok::<(), scirs2_io::error::IoError>(())
199/// ```
200#[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    // Convert to RGB
215    let rgb_img = img.to_rgb8();
216    let raw_data = rgb_img.into_raw();
217
218    // Convert to ndarray
219    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    // Try to read EXIF metadata
223    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/// Save image to file
238///
239/// # Arguments
240/// * `image_data` - ImageData to save
241/// * `path` - Output file path
242/// * `format` - Output format (optional, inferred from path if None)
243///
244/// # Example
245/// ```no_run
246/// use scirs2_io::image::{load_image, save_image, ImageFormat};
247///
248/// let image_data = load_image("input.jpg")?;
249/// save_image(&image_data, "output.png", Some(ImageFormat::PNG))?;
250/// # Ok::<(), scirs2_io::error::IoError>(())
251/// ```
252#[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/// Convert image to different format
286///
287/// # Arguments
288/// * `input_path` - Input image path
289/// * `output_path` - Output image path
290/// * `target_format` - Target format
291///
292/// # Example
293/// ```no_run
294/// use scirs2_io::image::{convert_image, ImageFormat};
295///
296/// convert_image("photo.jpg", "photo.png", ImageFormat::PNG)?;
297/// # Ok::<(), scirs2_io::error::IoError>(())
298/// ```
299#[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/// Resize image
310///
311/// # Arguments
312/// * `image_data` - Input image data
313/// * `new_width` - New width in pixels
314/// * `new_height` - New height in pixels
315///
316/// # Returns
317/// New ImageData with resized image
318///
319/// # Example
320/// ```no_run
321/// use scirs2_io::image::{load_image, resize_image};
322///
323/// let image_data = load_image("large_photo.jpg")?;
324/// let resized = resize_image(&image_data, 800, 600)?;
325/// # Ok::<(), scirs2_io::error::IoError>(())
326/// ```
327#[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/// Get basic image information without loading full data
356///
357/// # Arguments
358/// * `path` - Path to image file
359///
360/// # Returns
361/// ImageMetadata with basic information
362///
363/// # Example
364/// ```no_run
365/// use scirs2_io::image::get_image_info;
366///
367/// let info = get_image_info("photo.jpg")?;
368/// println!("Image: {}x{} pixels", info.width, info.height);
369/// # Ok::<(), scirs2_io::error::IoError>(())
370/// ```
371#[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    // Try to read EXIF metadata
392    let exif = read_exif_metadata(_path)?;
393
394    Ok(ImageMetadata {
395        width: dimensions.0,
396        height: dimensions.1,
397        color_mode: ColorMode::RGB, // Default assumption
398        format,
399        file_size,
400        exif,
401    })
402}
403
404/// Load animated image/GIF
405///
406/// # Arguments
407/// * `path` - Path to animated image file
408///
409/// # Returns
410/// AnimationData with all frames and timing information
411///
412/// # Example
413/// ```no_run
414/// use scirs2_io::image::load_animation;
415///
416/// let animation = load_animation("animated.gif")?;
417/// println!("Animation has {} frames", animation.frames.len());
418/// # Ok::<(), scirs2_io::error::IoError>(())
419/// ```
420#[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        // Convert to RGB if needed
442        let rgb_data = if raw_data.len() == (width * height * 4) as usize {
443            // RGBA to RGB conversion
444            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, // Unknown for individual frames
462            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, // Assume infinite loop
473    })
474}
475
476/// Read EXIF metadata from image file
477///
478/// # Arguments
479/// * `path` - Path to image file
480///
481/// # Returns
482/// Optional ExifMetadata if EXIF data exists and can be read
483///
484/// # Example
485/// ```no_run
486/// use scirs2_io::image::read_exif_metadata;
487///
488/// if let Some(exif) = read_exif_metadata("photo.jpg")? {
489///     if let Some(gps) = exif.gps {
490///         println!("GPS: {}, {}", gps.latitude.unwrap_or(0.0), gps.longitude.unwrap_or(0.0));
491///     }
492/// }
493/// # Ok::<(), scirs2_io::error::IoError>(())
494/// ```
495#[allow(dead_code)]
496pub fn read_exif_metadata<P: AsRef<Path>>(path: P) -> Result<Option<ExifMetadata>> {
497    let _path = path.as_ref();
498
499    // Try to read EXIF data using the `exif` crate
500    #[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), // File not found or permission denied
508        };
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), // No EXIF data or read error
515        };
516
517        let mut metadata = ExifMetadata::default();
518
519        // Extract datetime
520        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                        // Parse EXIF datetime format: "YYYY:MM:DD HH:MM:SS"
525                        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        // Extract GPS coordinates
537        let mut gps = GpsCoordinates::default();
538
539        // Latitude
540        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                        // Check hemisphere
555                        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        // Longitude
568        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                        // Check hemisphere
583                        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        // Altitude
596        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        // Extract camera information
609        let mut camera = CameraSettings::default();
610
611        // Camera make
612        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        // Camera model
623        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        // Lens model
634        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        // ISO
645        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        // Aperture (F-number)
656        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        // Shutter speed
665        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        // Focal length
674        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        // Flash
683        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); // Bit 0 indicates flash fired
687                }
688            }
689        }
690
691        // White balance
692        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        // Orientation
707        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        // Software
716        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        // Copyright
727        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        // Artist
738        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        // Image description
749        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        // Store raw tags for advanced users
760        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        // If EXIF feature is not enabled, return None
772        Ok(None)
773    }
774}
775
776/// Extract all images from a directory matching pattern
777///
778/// # Arguments
779/// * `dir_path` - Directory to search
780/// * `pattern` - File pattern (e.g., "*.jpg")
781/// * `recursive` - Whether to search subdirectories
782///
783/// # Returns
784/// Vector of image file paths
785///
786/// # Example
787/// ```no_run
788/// use scirs2_io::image::find_images;
789///
790/// let images = find_images("./photos", "*.{jpg,png}", true)?;
791/// for image_path in images {
792///     println!("Found: {}", image_path.display());
793/// }
794/// # Ok::<(), scirs2_io::error::IoError>(())
795/// ```
796#[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    // glob yields paths in sorted order; read_dir does not, so sort explicitly.
817    paths.sort();
818    Ok(paths)
819}
820
821/// Recursively (or not) collect files under `dir` whose file name matches the
822/// glob `pattern`. Unreadable directories and entries are silently skipped,
823/// mirroring the previous `glob::glob(...).filter_map(|e| e.ok())` behaviour.
824fn 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
848/// Match a file name against a glob pattern supporting `*`, `?` and `[...]`
849/// character classes (with `!` negation and `a-z` ranges), as the `glob`
850/// crate does for a single path component. Matching is case-sensitive.
851fn 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    // Backtracking state for the most recent `*`.
856    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                // Unterminated class: treat '[' as a literal, like a mismatch
877                // unless it equals the character.
878                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            // Backtrack: let the last `*` consume one more character.
889            star_t += 1;
890            t = star_t;
891            p = sp + 1;
892        } else {
893            return false;
894        }
895    }
896    // Remaining pattern must be all `*` to match.
897    pat[p..].iter().all(|&c| c == '*')
898}
899
900/// Try to match `c` against the character class starting at `pat[start]`
901/// (which must be `'['`). Returns `Some((matched, index_after_class))`, or
902/// `None` if the class is unterminated.
903fn 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        // Range like `a-z` (the `-` must not be the final char before `]`).
917        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/// Batch process images in directory
935///
936/// # Arguments
937/// * `input_dir` - Input directory path
938/// * `output_dir` - Output directory path
939/// * `processor` - Function to process each image
940///
941/// # Example
942/// ```no_run
943/// use scirs2_io::image::{batch_process_images, ImageData, resize_image};
944///
945/// batch_process_images(
946///     "./input",
947///     "./output",
948///     |image_data| resize_image(image_data, 800, 600)
949/// )?;
950/// # Ok::<(), scirs2_io::error::IoError>(())
951/// ```
952#[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    // Create output directory if it doesn't exist
963    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        // `*` wildcard
994        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        // `?` wildcard
998        assert!(glob_match_file_name("img?.png", "img1.png"));
999        assert!(!glob_match_file_name("img?.png", "img10.png"));
1000        // character classes with ranges and negation
1001        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        // literal match and case sensitivity
1005        assert!(glob_match_file_name("exact.bmp", "exact.bmp"));
1006        assert!(!glob_match_file_name("*.JPG", "photo.jpg"));
1007        // multiple stars with backtracking
1008        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}