Skip to main content

exif_oxide/processor_registry/processors/
canon.rs

1//! Canon-specific BinaryDataProcessor implementations
2//!
3//! These processors implement the BinaryDataProcessor trait for Canon camera data
4//! while delegating to the existing Canon implementation modules. This maintains
5//! the "Trust ExifTool" principle by reusing proven processing logic.
6//!
7//! ## ExifTool Reference
8//!
9//! Canon.pm ProcessBinaryData, ProcessSerialData, and related processing functions
10
11use super::super::{
12    BinaryDataProcessor, ProcessorCapability, ProcessorContext, ProcessorMetadata, ProcessorResult,
13};
14use crate::implementations::canon;
15use crate::types::{Result, TagValue};
16use std::collections::HashMap;
17use tracing::debug;
18
19/// Canon Serial Data processor using existing implementation
20///
21/// Processes Canon camera serial data using the existing `canon::binary_data`
22/// implementation. This processor is selected for Canon cameras with table
23/// names containing "SerialData".
24///
25/// ## ExifTool Reference
26///
27/// Canon.pm ProcessSerialData function and related binary data processing
28pub struct CanonSerialDataProcessor;
29
30impl BinaryDataProcessor for CanonSerialDataProcessor {
31    fn can_process(&self, context: &ProcessorContext) -> ProcessorCapability {
32        // Perfect match if Canon manufacturer and SerialData table
33        if context.is_manufacturer("Canon") && context.table_name.contains("SerialData") {
34            return ProcessorCapability::Perfect;
35        }
36
37        // Good match for Canon with any binary data table
38        if context.is_manufacturer("Canon") && context.table_name.contains("Binary") {
39            return ProcessorCapability::Good;
40        }
41
42        // Only compatible with Canon-specific tables
43        ProcessorCapability::Incompatible
44    }
45
46    fn process_data(&self, data: &[u8], context: &ProcessorContext) -> Result<ProcessorResult> {
47        debug!(
48            "Processing Canon SerialData with {} bytes for table: {}",
49            data.len(),
50            context.table_name
51        );
52
53        let mut result = ProcessorResult::new();
54
55        // Delegate to existing Canon binary data processing
56        // This extracts camera settings and serial data using proven ExifTool logic
57        let byte_order = context
58            .byte_order
59            .unwrap_or(crate::tiff_types::ByteOrder::LittleEndian);
60        let camera_settings = canon::binary_data::extract_camera_settings(
61            data,
62            context.data_offset,
63            data.len(),
64            byte_order,
65        );
66
67        match camera_settings {
68            Ok(tags) => {
69                // Convert HashMap<String, TagValue> to ProcessorResult format
70                for (tag_name, tag_value) in tags {
71                    result.add_tag(tag_name, tag_value);
72                }
73
74                debug!(
75                    "Canon SerialData processor extracted {} tags",
76                    result.extracted_tags.len()
77                );
78            }
79            Err(e) => {
80                let warning = format!("Canon SerialData processing failed: {e}");
81                result.add_warning(warning);
82                debug!("Canon SerialData processing error: {}", e);
83            }
84        }
85
86        Ok(result)
87    }
88
89    fn get_metadata(&self) -> ProcessorMetadata {
90        ProcessorMetadata::new(
91            "Canon Serial Data Processor".to_string(),
92            "Processes Canon camera serial data using existing implementation".to_string(),
93        )
94        .with_manufacturer("Canon".to_string())
95        .with_required_context("manufacturer".to_string())
96        .with_example_condition(
97            "manufacturer == 'Canon' && table.contains('SerialData')".to_string(),
98        )
99    }
100}
101
102/// Canon Camera Settings processor for binary data tables
103///
104/// Processes Canon CameraSettings binary data using the existing implementation.
105/// This handles the complex binary data formats used by Canon cameras.
106///
107/// ## ExifTool Reference
108///
109/// Canon.pm CameraSettings table processing with ProcessBinaryData
110pub struct CanonCameraSettingsProcessor;
111
112impl BinaryDataProcessor for CanonCameraSettingsProcessor {
113    fn can_process(&self, context: &ProcessorContext) -> ProcessorCapability {
114        // Only process Canon-specific tables, not standard EXIF directories
115        // ExifTool Canon.pm only processes Canon:: prefixed tables
116        if !context.is_manufacturer("Canon") {
117            return ProcessorCapability::Incompatible;
118        }
119
120        // Perfect match for Canon CameraSettings table
121        if context.table_name == "Canon::CameraSettings" {
122            return ProcessorCapability::Perfect;
123        }
124
125        // Good match for Canon binary data tables that start with Canon::
126        if context.table_name.starts_with("Canon::") && context.table_name.contains("Settings") {
127            return ProcessorCapability::Good;
128        }
129
130        // Incompatible with non-Canon tables (like ExifIFD, GPS, etc.)
131        ProcessorCapability::Incompatible
132    }
133
134    fn process_data(&self, data: &[u8], context: &ProcessorContext) -> Result<ProcessorResult> {
135        debug!(
136            "Processing Canon CameraSettings with {} bytes for table: {}",
137            data.len(),
138            context.table_name
139        );
140
141        let mut result = ProcessorResult::new();
142
143        // Create the Canon CameraSettings table using existing implementation
144        let table = canon::binary_data::create_canon_camera_settings_table();
145
146        // Use existing binary data extraction logic
147        // This mimics what an ExifReader would do with the table
148        let extracted_tags = extract_binary_data_using_table(data, context, &table)?;
149
150        for (tag_name, tag_value) in extracted_tags {
151            result.add_tag(tag_name, tag_value);
152        }
153
154        if result.extracted_tags.is_empty() {
155            result.add_warning("No Canon CameraSettings tags extracted".to_string());
156        } else {
157            debug!(
158                "Canon CameraSettings processor extracted {} tags",
159                result.extracted_tags.len()
160            );
161        }
162
163        Ok(result)
164    }
165
166    fn get_metadata(&self) -> ProcessorMetadata {
167        ProcessorMetadata::new(
168            "Canon Camera Settings Processor".to_string(),
169            "Processes Canon CameraSettings binary data table".to_string(),
170        )
171        .with_manufacturer("Canon".to_string())
172        .with_required_context("manufacturer".to_string())
173        .with_example_condition(
174            "manufacturer == 'Canon' && table.contains('CameraSettings')".to_string(),
175        )
176    }
177}
178
179/// Enhanced Canon processor for newer models with additional data
180///
181/// Handles Canon R5, R6, and other newer models that have enhanced
182/// binary data formats and additional metadata fields.
183///
184/// ## ExifTool Reference
185///
186/// Canon.pm model-specific processing for newer camera bodies
187pub struct CanonSerialDataMkIIProcessor;
188
189impl BinaryDataProcessor for CanonSerialDataMkIIProcessor {
190    fn can_process(&self, context: &ProcessorContext) -> ProcessorCapability {
191        // Perfect match for newer Canon models with enhanced data
192        if context.is_manufacturer("Canon") {
193            if let Some(model) = &context.model {
194                if (model.contains("EOS R5")
195                    || model.contains("EOS R6")
196                    || model.contains("EOS R3"))
197                    && context.table_name.contains("SerialData")
198                {
199                    return ProcessorCapability::Perfect;
200                }
201            }
202        }
203
204        ProcessorCapability::Incompatible
205    }
206
207    fn process_data(&self, data: &[u8], context: &ProcessorContext) -> Result<ProcessorResult> {
208        debug!(
209            "Processing Canon MkII SerialData with {} bytes for model: {:?}",
210            data.len(),
211            context.model
212        );
213
214        let mut result = ProcessorResult::new();
215
216        // Enhanced serial data processing for newer Canon models
217        if data.len() < 12 {
218            result.add_warning("Canon MkII serial data too short".to_string());
219            return Ok(result);
220        }
221
222        // Extract extended serial information (following ExifTool patterns)
223        // ExifTool: Enhanced data extraction for newer models
224
225        // Serial number (first 8 bytes)
226        let serial_bytes = &data[0..8];
227        let serial_number = String::from_utf8_lossy(serial_bytes)
228            .trim_end_matches('\0')
229            .to_string();
230        if !serial_number.is_empty() {
231            result.add_tag("SerialNumber".to_string(), TagValue::String(serial_number));
232        }
233
234        // Firmware version (bytes 8-11) - enhanced format for newer models
235        if data.len() >= 12 {
236            let firmware_major = data[8];
237            let firmware_minor = data[9];
238            let firmware_patch = u16::from_le_bytes([data[10], data[11]]);
239
240            let firmware_version = format!("{firmware_major}.{firmware_minor}.{firmware_patch}");
241            result.add_tag(
242                "FirmwareVersion".to_string(),
243                TagValue::String(firmware_version),
244            );
245        }
246
247        // Additional model-specific data extraction could go here
248        if let Some(model) = &context.model {
249            result.add_tag("CameraModel".to_string(), TagValue::String(model.clone()));
250        }
251
252        debug!(
253            "Canon MkII SerialData processor extracted {} tags",
254            result.extracted_tags.len()
255        );
256
257        Ok(result)
258    }
259
260    fn get_metadata(&self) -> ProcessorMetadata {
261        ProcessorMetadata::new(
262            "Canon Serial Data MkII".to_string(),
263            "Enhanced serial data processing for newer Canon models (R5, R6, R3)".to_string(),
264        )
265        .with_manufacturer("Canon".to_string())
266        .with_required_context("manufacturer".to_string())
267        .with_required_context("model".to_string())
268        .with_example_condition(
269            "manufacturer == 'Canon' && (model.contains('EOS R5') || model.contains('EOS R6'))"
270                .to_string(),
271        )
272    }
273}
274
275/// Helper function to extract binary data using a Canon table definition
276///
277/// This function mimics what ExifReader.process_binary_data would do but works
278/// independently for processor implementations.
279fn extract_binary_data_using_table(
280    data: &[u8],
281    context: &ProcessorContext,
282    table: &crate::types::BinaryDataTable,
283) -> Result<HashMap<String, TagValue>> {
284    let mut extracted = HashMap::new();
285
286    // Process each defined tag in the table
287    for (&index, tag_def) in &table.tags {
288        // Calculate position based on FIRST_ENTRY
289        let first_entry = table.first_entry.unwrap_or(0);
290        if index < first_entry {
291            continue;
292        }
293
294        let entry_offset = (index - first_entry) as usize * table.default_format.byte_size();
295        if entry_offset + table.default_format.byte_size() > data.len() {
296            debug!("Tag {} at index {} beyond data bounds", tag_def.name, index);
297            continue;
298        }
299
300        let data_offset = context.data_offset + entry_offset;
301
302        // Extract the raw value using the table's default format
303        let format = tag_def.format.unwrap_or(table.default_format);
304        let byte_order = context
305            .byte_order
306            .unwrap_or(crate::tiff_types::ByteOrder::LittleEndian);
307        let raw_value = extract_binary_value_direct(data, data_offset, format, byte_order)?;
308
309        // Apply PrintConv if available
310        let final_value = if let Some(print_conv) = &tag_def.print_conv {
311            apply_print_conv(&raw_value, print_conv)
312        } else {
313            raw_value
314        };
315
316        // Store with group prefix like ExifTool
317        let group_0 = table
318            .groups
319            .get(&0)
320            .cloned()
321            .unwrap_or_else(|| "Unknown".to_string());
322
323        let tag_name = format!("{}:{}", group_0, tag_def.name);
324
325        debug!(
326            "Extracted Canon binary tag {} (index {}) = {:?}",
327            tag_def.name, index, final_value
328        );
329
330        extracted.insert(tag_name, final_value);
331    }
332
333    Ok(extracted)
334}
335
336/// Extract binary value directly from data
337///
338/// This is a simplified version of the binary value extraction that works
339/// without requiring a full ExifReader instance.
340fn extract_binary_value_direct(
341    data: &[u8],
342    offset: usize,
343    format: crate::types::BinaryDataFormat,
344    byte_order: crate::tiff_types::ByteOrder,
345) -> Result<TagValue> {
346    use crate::types::{BinaryDataFormat, ExifError};
347
348    match format {
349        BinaryDataFormat::Int8u => {
350            if offset >= data.len() {
351                return Err(ExifError::ParseError(
352                    "Offset beyond data bounds".to_string(),
353                ));
354            }
355            Ok(TagValue::U8(data[offset]))
356        }
357        BinaryDataFormat::Int16s => {
358            if offset + 2 > data.len() {
359                return Err(ExifError::ParseError(
360                    "Offset beyond data bounds for int16s".to_string(),
361                ));
362            }
363            let value = byte_order.read_u16(data, offset)? as i16;
364            Ok(TagValue::I16(value))
365        }
366        BinaryDataFormat::Int16u => {
367            if offset + 2 > data.len() {
368                return Err(ExifError::ParseError(
369                    "Offset beyond data bounds for int16u".to_string(),
370                ));
371            }
372            let value = byte_order.read_u16(data, offset)?;
373            Ok(TagValue::U16(value))
374        }
375        BinaryDataFormat::Int32u => {
376            if offset + 4 > data.len() {
377                return Err(ExifError::ParseError(
378                    "Offset beyond data bounds for int32u".to_string(),
379                ));
380            }
381            let value = byte_order.read_u32(data, offset)?;
382            Ok(TagValue::U32(value))
383        }
384        BinaryDataFormat::String => {
385            if offset >= data.len() {
386                return Err(ExifError::ParseError(
387                    "Offset beyond data bounds for string".to_string(),
388                ));
389            }
390
391            let mut end = offset;
392            while end < data.len() && data[end] != 0 {
393                end += 1;
394            }
395
396            let string_bytes = &data[offset..end];
397            let string_value = String::from_utf8_lossy(string_bytes).to_string();
398            Ok(TagValue::String(string_value))
399        }
400        _ => Err(ExifError::ParseError(format!(
401            "Binary format {format:?} not yet implemented"
402        ))),
403    }
404}
405
406/// Apply PrintConv lookup to a raw value
407fn apply_print_conv(raw_value: &TagValue, print_conv: &HashMap<u32, String>) -> TagValue {
408    match raw_value {
409        TagValue::I16(val) => {
410            if let Some(converted) = print_conv.get(&(*val as u32)) {
411                TagValue::String(converted.clone())
412            } else {
413                raw_value.clone()
414            }
415        }
416        TagValue::U16(val) => {
417            if let Some(converted) = print_conv.get(&(*val as u32)) {
418                TagValue::String(converted.clone())
419            } else {
420                raw_value.clone()
421            }
422        }
423        TagValue::U32(val) => {
424            if let Some(converted) = print_conv.get(val) {
425                TagValue::String(converted.clone())
426            } else {
427                raw_value.clone()
428            }
429        }
430        _ => raw_value.clone(),
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::formats::FileFormat;
438    use crate::processor_registry::ProcessorContext;
439
440    #[test]
441    fn test_canon_serial_data_processor_capability() {
442        let processor = CanonSerialDataProcessor;
443
444        // Perfect match for Canon SerialData
445        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::SerialData".to_string())
446            .with_manufacturer("Canon".to_string());
447
448        assert_eq!(
449            processor.can_process(&context),
450            ProcessorCapability::Perfect
451        );
452
453        // Incompatible for non-Canon
454        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::SerialData".to_string())
455            .with_manufacturer("Nikon".to_string());
456
457        assert_eq!(
458            processor.can_process(&context),
459            ProcessorCapability::Incompatible
460        );
461    }
462
463    #[test]
464    fn test_canon_mk2_processor_capability() {
465        let processor = CanonSerialDataMkIIProcessor;
466
467        // Perfect match for Canon R5
468        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::SerialData".to_string())
469            .with_manufacturer("Canon".to_string())
470            .with_model("Canon EOS R5".to_string());
471
472        assert_eq!(
473            processor.can_process(&context),
474            ProcessorCapability::Perfect
475        );
476
477        // Incompatible for older Canon models
478        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::SerialData".to_string())
479            .with_manufacturer("Canon".to_string())
480            .with_model("Canon EOS 5D Mark IV".to_string());
481
482        assert_eq!(
483            processor.can_process(&context),
484            ProcessorCapability::Incompatible
485        );
486    }
487
488    #[test]
489    fn test_canon_processor_metadata() {
490        let processor = CanonSerialDataProcessor;
491        let metadata = processor.get_metadata();
492
493        assert_eq!(metadata.name, "Canon Serial Data Processor");
494        assert!(metadata
495            .supported_manufacturers
496            .contains(&"Canon".to_string()));
497        assert!(metadata
498            .required_context
499            .contains(&"manufacturer".to_string()));
500    }
501}