exif_oxide/formats/
detection.rs

1//! File format detection using magic bytes
2//!
3//! This module handles file format detection using magic bytes and provides
4//! utilities for determining file types reliably. Implements magic byte
5//! detection for JPEG and TIFF formats following ExifTool patterns.
6
7use crate::types::{ExifError, Result};
8use std::fs::File;
9use std::io::{BufReader, Read, Seek, SeekFrom};
10use std::path::Path;
11
12/// Detect file format using magic bytes (primary) with extension fallback
13///
14/// Reads the first few bytes of the file to identify format by magic signature.
15/// This is more reliable than extension-based detection.
16pub fn detect_file_format<R: Read + Seek>(mut reader: R) -> Result<FileFormat> {
17    let mut magic_bytes = [0u8; 4];
18    reader.read_exact(&mut magic_bytes)?;
19
20    // Reset to beginning for subsequent reading
21    reader.seek(SeekFrom::Start(0))?;
22
23    match &magic_bytes[0..2] {
24        // JPEG magic bytes: 0xFFD8
25        [0xFF, 0xD8] => Ok(FileFormat::Jpeg),
26        // TIFF magic bytes: "II" (little-endian) or "MM" (big-endian)
27        [0x49, 0x49] | [0x4D, 0x4D] => Ok(FileFormat::Tiff),
28        _ => {
29            // Check for other formats by examining more bytes
30            Err(ExifError::Unsupported(
31                "Unsupported file format - not a JPEG or TIFF".to_string(),
32            ))
33        }
34    }
35}
36
37/// Convenience function to detect format from file path
38pub fn detect_file_format_from_path(path: &Path) -> Result<FileFormat> {
39    let file = File::open(path)?;
40    let reader = BufReader::new(file);
41    detect_file_format(reader)
42}
43
44/// Supported file formats
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum FileFormat {
47    Jpeg,
48    Tiff,
49    CanonRaw,
50    NikonRaw,
51    SonyRaw,
52    Dng,
53}
54
55impl FileFormat {
56    /// Get the MIME type for this format
57    pub fn mime_type(&self) -> &'static str {
58        match self {
59            FileFormat::Jpeg => "image/jpeg",
60            FileFormat::Tiff => "image/tiff",
61            FileFormat::CanonRaw => "image/x-canon-cr2",
62            FileFormat::NikonRaw => "image/x-nikon-nef",
63            FileFormat::SonyRaw => "image/x-sony-arw",
64            FileFormat::Dng => "image/x-adobe-dng",
65        }
66    }
67
68    /// Get the typical file extension
69    pub fn extension(&self) -> &'static str {
70        match self {
71            FileFormat::Jpeg => "jpg",
72            FileFormat::Tiff => "tif",
73            FileFormat::CanonRaw => "cr2",
74            FileFormat::NikonRaw => "nef",
75            FileFormat::SonyRaw => "arw",
76            FileFormat::Dng => "dng",
77        }
78    }
79
80    /// Get ExifTool-compatible FileType name
81    /// ExifTool.pm %fileTypeLookup hash maps extensions to these type names
82    /// See: https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool.pm#L229-L580
83    pub fn file_type(&self) -> &'static str {
84        match self {
85            FileFormat::Jpeg => "JPEG",
86            FileFormat::Tiff => "TIFF",
87            FileFormat::CanonRaw => "CR2",
88            FileFormat::NikonRaw => "NEF",
89            FileFormat::SonyRaw => "ARW",
90            FileFormat::Dng => "DNG",
91        }
92    }
93
94    /// Get ExifTool-compatible FileTypeExtension
95    /// Based on ExifTool.pm %fileTypeExt hash overrides
96    /// ExifTool.pm:582-592 - Special cases where extension differs from FileType
97    /// See: https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool.pm#L582-L592
98    /// Note: ExifTool does NOT uppercase the extension despite the uc() call in FoundTag
99    pub fn file_type_extension(&self) -> &'static str {
100        match self {
101            // ExifTool %fileTypeExt overrides: 'JPEG' => 'jpg', 'TIFF' => 'tif'
102            FileFormat::Jpeg => "jpg", // ExifTool: FileType "JPEG" → Extension "jpg"
103            FileFormat::Tiff => "tif", // ExifTool: FileType "TIFF" → Extension "tif"
104            // For other formats, FileTypeExtension = lowercase(FileType)
105            FileFormat::CanonRaw => "cr2",
106            FileFormat::NikonRaw => "nef",
107            FileFormat::SonyRaw => "arw",
108            FileFormat::Dng => "dng",
109        }
110    }
111}
112
113/// Get format properties for validation and processing
114pub fn get_format_properties(format: FileFormat) -> FormatProperties {
115    FormatProperties {
116        mime_type: format.mime_type(),
117        extension: format.extension(),
118        supports_exif: matches!(format, FileFormat::Jpeg | FileFormat::Tiff),
119        supports_makernotes: matches!(format, FileFormat::Jpeg | FileFormat::Tiff),
120    }
121}
122
123/// Properties for a detected file format
124#[derive(Debug, Clone)]
125pub struct FormatProperties {
126    pub mime_type: &'static str,
127    pub extension: &'static str,
128    pub supports_exif: bool,
129    pub supports_makernotes: bool,
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::io::Cursor;
136
137    #[test]
138    fn test_jpeg_magic_bytes() {
139        let jpeg_magic = [0xFF, 0xD8, 0xFF, 0xE0]; // JPEG magic bytes
140        let cursor = Cursor::new(jpeg_magic);
141        let format = detect_file_format(cursor).unwrap();
142        assert_eq!(format, FileFormat::Jpeg);
143    }
144
145    #[test]
146    fn test_tiff_magic_bytes_little_endian() {
147        let tiff_magic = [0x49, 0x49, 0x2A, 0x00]; // TIFF LE magic bytes
148        let cursor = Cursor::new(tiff_magic);
149        let format = detect_file_format(cursor).unwrap();
150        assert_eq!(format, FileFormat::Tiff);
151    }
152
153    #[test]
154    fn test_tiff_magic_bytes_big_endian() {
155        let tiff_magic = [0x4D, 0x4D, 0x00, 0x2A]; // TIFF BE magic bytes
156        let cursor = Cursor::new(tiff_magic);
157        let format = detect_file_format(cursor).unwrap();
158        assert_eq!(format, FileFormat::Tiff);
159    }
160
161    #[test]
162    fn test_unsupported_format() {
163        let unknown_magic = [0x12, 0x34, 0x56, 0x78];
164        let cursor = Cursor::new(unknown_magic);
165        let result = detect_file_format(cursor);
166        assert!(result.is_err());
167    }
168
169    #[test]
170    fn test_format_properties() {
171        let jpeg_props = get_format_properties(FileFormat::Jpeg);
172        assert_eq!(jpeg_props.mime_type, "image/jpeg");
173        assert_eq!(jpeg_props.extension, "jpg");
174        assert!(jpeg_props.supports_exif);
175        assert!(jpeg_props.supports_makernotes);
176
177        let tiff_props = get_format_properties(FileFormat::Tiff);
178        assert_eq!(tiff_props.mime_type, "image/tiff");
179        assert_eq!(tiff_props.extension, "tif");
180        assert!(tiff_props.supports_exif);
181        assert!(tiff_props.supports_makernotes);
182    }
183}