ricecoder_images/
formats.rs

1//! Image format validation and detection.
2
3use crate::error::{ImageError, ImageResult};
4use std::path::Path;
5
6/// Supported image formats.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ImageFormat {
9    /// PNG format
10    Png,
11    /// JPEG format
12    Jpeg,
13    /// GIF format
14    Gif,
15    /// WebP format
16    WebP,
17}
18
19impl ImageFormat {
20    /// Detect image format from file header (magic bytes).
21    pub fn detect_from_file(path: &Path) -> ImageResult<Self> {
22        let bytes = std::fs::read(path)?;
23        Self::detect_from_bytes(&bytes)
24    }
25
26    /// Detect image format from bytes (magic bytes).
27    pub fn detect_from_bytes(bytes: &[u8]) -> ImageResult<Self> {
28        if bytes.len() < 4 {
29            return Err(ImageError::InvalidFile(
30                "File too small to be a valid image".to_string(),
31            ));
32        }
33
34        // PNG: 89 50 4E 47
35        if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
36            return Ok(ImageFormat::Png);
37        }
38
39        // JPEG: FF D8 FF
40        if bytes.len() >= 3 && bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
41            return Ok(ImageFormat::Jpeg);
42        }
43
44        // GIF: 47 49 46 (GIF87a or GIF89a)
45        if bytes.starts_with(b"GIF") {
46            return Ok(ImageFormat::Gif);
47        }
48
49        // WebP: RIFF ... WEBP
50        if bytes.len() >= 12
51            && bytes.starts_with(b"RIFF")
52            && bytes[8..12] == *b"WEBP"
53        {
54            return Ok(ImageFormat::WebP);
55        }
56
57        Err(ImageError::InvalidFile(
58            "Unable to detect image format from file header".to_string(),
59        ))
60    }
61
62    /// Get the format as a string.
63    pub fn as_str(&self) -> &'static str {
64        match self {
65            ImageFormat::Png => "png",
66            ImageFormat::Jpeg => "jpg",
67            ImageFormat::Gif => "gif",
68            ImageFormat::WebP => "webp",
69        }
70    }
71
72    /// Validate image file format and size.
73    ///
74    /// # Arguments
75    ///
76    /// * `path` - Path to the image file
77    /// * `max_size_mb` - Maximum allowed file size in MB
78    ///
79    /// # Returns
80    ///
81    /// Image format if valid, error otherwise
82    pub fn validate_file(path: &Path, max_size_mb: u64) -> ImageResult<Self> {
83        // Check file exists
84        if !path.exists() {
85            return Err(ImageError::InvalidFile(
86                "File does not exist".to_string(),
87            ));
88        }
89
90        // Check file size
91        let metadata = std::fs::metadata(path)?;
92        let size_mb = metadata.len() / (1024 * 1024);
93        if size_mb > max_size_mb {
94            return Err(ImageError::FileTooLarge {
95                size_mb: size_mb as f64,
96            });
97        }
98
99        // Detect format
100        Self::detect_from_file(path)
101    }
102
103    /// Extract image metadata (width, height).
104    pub fn extract_metadata(path: &Path) -> ImageResult<(u32, u32)> {
105        let img = image::open(path).map_err(|e| {
106            ImageError::InvalidFile(format!("Failed to open image: {}", e))
107        })?;
108
109        Ok((img.width(), img.height()))
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_detect_png_format() {
119        let png_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
120        let format = ImageFormat::detect_from_bytes(&png_bytes).unwrap();
121        assert_eq!(format, ImageFormat::Png);
122    }
123
124    #[test]
125    fn test_detect_jpeg_format() {
126        let jpeg_bytes = vec![0xFF, 0xD8, 0xFF, 0xE0];
127        let format = ImageFormat::detect_from_bytes(&jpeg_bytes).unwrap();
128        assert_eq!(format, ImageFormat::Jpeg);
129    }
130
131    #[test]
132    fn test_detect_gif_format() {
133        let gif_bytes = b"GIF89a".to_vec();
134        let format = ImageFormat::detect_from_bytes(&gif_bytes).unwrap();
135        assert_eq!(format, ImageFormat::Gif);
136    }
137
138    #[test]
139    fn test_detect_webp_format() {
140        let mut webp_bytes = b"RIFF".to_vec();
141        webp_bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
142        webp_bytes.extend_from_slice(b"WEBP");
143        let format = ImageFormat::detect_from_bytes(&webp_bytes).unwrap();
144        assert_eq!(format, ImageFormat::WebP);
145    }
146
147    #[test]
148    fn test_invalid_format() {
149        let invalid_bytes = vec![0x00, 0x00, 0x00, 0x00];
150        let result = ImageFormat::detect_from_bytes(&invalid_bytes);
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_format_as_str() {
156        assert_eq!(ImageFormat::Png.as_str(), "png");
157        assert_eq!(ImageFormat::Jpeg.as_str(), "jpg");
158        assert_eq!(ImageFormat::Gif.as_str(), "gif");
159        assert_eq!(ImageFormat::WebP.as_str(), "webp");
160    }
161}