Skip to main content

exif_oxide/raw/formats/
kyocera.rs

1//! Kyocera RAW format handler
2//!
3//! This module implements ExifTool's KyoceraRaw.pm processing logic exactly.
4//! Kyocera Contax N Digital cameras store RAW metadata in a simple 156-byte
5//! binary header with string values stored in byte-reversed format.
6//!
7//! ExifTool Reference: lib/Image/ExifTool/KyoceraRaw.pm (173 lines total)
8//! Processing: ProcessBinaryData with fixed offsets and string reversal
9
10use crate::exif::ExifReader;
11use crate::raw::{utils, RawFormatHandler};
12use crate::types::{ExifError, Result, TagSourceInfo, TagValue};
13use std::collections::HashMap;
14
15/// Kyocera RAW format handler
16/// ExifTool: lib/Image/ExifTool/KyoceraRaw.pm - Simple ProcessBinaryData format
17pub struct KyoceraRawHandler {
18    /// Tag definitions with offsets and formats
19    /// ExifTool: %Image::ExifTool::KyoceraRaw::Main hash (lines 25-106)
20    tag_definitions: HashMap<u16, KyoceraTagDef>,
21}
22
23/// Kyocera tag definition
24/// ExifTool: Individual entries in %Image::ExifTool::KyoceraRaw::Main
25#[derive(Debug, Clone)]
26struct KyoceraTagDef {
27    /// Tag name
28    /// ExifTool: Key in tag hash
29    #[allow(dead_code)] // Used for debugging and future reference
30    name: String,
31    /// Offset in binary data
32    /// ExifTool: Tag number/offset
33    offset: usize,
34    /// Data format
35    /// ExifTool: Format field
36    format: KyoceraFormat,
37    /// Optional conversion function
38    /// ExifTool: ValueConv/PrintConv fields
39    conversion: Option<KyoceraConversion>,
40}
41
42/// Kyocera data formats
43/// ExifTool: Format specifications in tag definitions
44#[derive(Debug, Clone)]
45enum KyoceraFormat {
46    /// Fixed-length string (with byte count)
47    /// ExifTool: string[N] format
48    String(usize),
49    /// 32-bit unsigned integer
50    /// ExifTool: int32u format
51    Int32u,
52    /// Array of 32-bit unsigned integers
53    /// ExifTool: int32u[N] format
54    Int32uArray(usize),
55}
56
57/// Kyocera conversion types
58/// ExifTool: ValueConv and PrintConv logic
59#[derive(Debug, Clone)]
60enum KyoceraConversion {
61    /// Reverse string bytes (Kyocera-specific)
62    /// ExifTool: \&ReverseString function reference
63    ReverseString,
64    /// Convert to exposure time
65    /// ExifTool: '2**($val / 8) / 16000' expression
66    ExposureTime,
67    /// Convert to F-number
68    /// ExifTool: '2**($val / 16)' expression
69    FNumber,
70    /// Convert to max aperture
71    /// ExifTool: '2**($val / 16)' expression (same as FNumber)
72    MaxAperture,
73    /// Convert to datetime
74    /// ExifTool: '$self->ConvertDateTime($val)' expression
75    DateTime,
76    /// Convert to focal length with "mm" suffix
77    /// ExifTool: '"$val mm"' expression
78    FocalLength,
79    /// Convert using ISO lookup table
80    /// ExifTool: PrintConv hash with ISO values (lines 56-70)
81    IsoLookup,
82}
83
84impl Default for KyoceraRawHandler {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl KyoceraRawHandler {
91    /// Create new Kyocera RAW handler with tag definitions
92    /// ExifTool: %Image::ExifTool::KyoceraRaw::Main hash construction
93    pub fn new() -> Self {
94        let mut tag_definitions = HashMap::new();
95
96        // ExifTool: lib/Image/ExifTool/KyoceraRaw.pm lines 26-106
97        // Each tag definition is translated exactly from the Perl hash
98
99        // FirmwareVersion at offset 0x01
100        // ExifTool: line 26 - 1 => { Name => 'FirmwareVersion', Format => 'string[10]', ValueConv => \&ReverseString }
101        tag_definitions.insert(
102            0x01,
103            KyoceraTagDef {
104                name: "FirmwareVersion".to_string(),
105                offset: 0x01,
106                format: KyoceraFormat::String(10),
107                conversion: Some(KyoceraConversion::ReverseString),
108            },
109        );
110
111        // Model at offset 0x0c
112        // ExifTool: line 27 - 0x0c => { Name => 'Model', Format => 'string[12]', ValueConv => \&ReverseString }
113        tag_definitions.insert(
114            0x0c,
115            KyoceraTagDef {
116                name: "Model".to_string(),
117                offset: 0x0c,
118                format: KyoceraFormat::String(12),
119                conversion: Some(KyoceraConversion::ReverseString),
120            },
121        );
122
123        // Make at offset 0x19
124        // ExifTool: line 28 - 0x19 => { Name => 'Make', Format => 'string[7]', ValueConv => \&ReverseString }
125        tag_definitions.insert(
126            0x19,
127            KyoceraTagDef {
128                name: "Make".to_string(),
129                offset: 0x19,
130                format: KyoceraFormat::String(7),
131                conversion: Some(KyoceraConversion::ReverseString),
132            },
133        );
134
135        // DateTimeOriginal at offset 0x21
136        // ExifTool: line 29-33 - 0x21 => { Name => 'DateTimeOriginal', Format => 'string[20]', Groups => { 2 => 'Time' }, ValueConv => \&ReverseString, PrintConv => '$self->ConvertDateTime($val)' }
137        tag_definitions.insert(
138            0x21,
139            KyoceraTagDef {
140                name: "DateTimeOriginal".to_string(),
141                offset: 0x21,
142                format: KyoceraFormat::String(20),
143                conversion: Some(KyoceraConversion::DateTime),
144            },
145        );
146
147        // ISO at offset 0x34
148        // ExifTool: lines 34-71 - 0x34 => { Name => 'ISO', Format => 'int32u', PrintConv => { hash with ISO values } }
149        tag_definitions.insert(
150            0x34,
151            KyoceraTagDef {
152                name: "ISO".to_string(),
153                offset: 0x34,
154                format: KyoceraFormat::Int32u,
155                conversion: Some(KyoceraConversion::IsoLookup),
156            },
157        );
158
159        // ExposureTime at offset 0x38
160        // ExifTool: lines 72-75 - 0x38 => { Name => 'ExposureTime', Format => 'int32u', ValueConv => '2**($val / 8) / 16000', PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)' }
161        tag_definitions.insert(
162            0x38,
163            KyoceraTagDef {
164                name: "ExposureTime".to_string(),
165                offset: 0x38,
166                format: KyoceraFormat::Int32u,
167                conversion: Some(KyoceraConversion::ExposureTime),
168            },
169        );
170
171        // WB_RGGBLevels at offset 0x3c
172        // ExifTool: line 76 - 0x3c => { Name => 'WB_RGGBLevels', Format => 'int32u[4]' }
173        tag_definitions.insert(
174            0x3c,
175            KyoceraTagDef {
176                name: "WB_RGGBLevels".to_string(),
177                offset: 0x3c,
178                format: KyoceraFormat::Int32uArray(4),
179                conversion: None,
180            },
181        );
182
183        // FNumber at offset 0x58
184        // ExifTool: lines 77-79 - 0x58 => { Name => 'FNumber', Format => 'int32u', ValueConv => '2**($val / 16)', PrintConv => 'sprintf("%.1f",$val)' }
185        tag_definitions.insert(
186            0x58,
187            KyoceraTagDef {
188                name: "FNumber".to_string(),
189                offset: 0x58,
190                format: KyoceraFormat::Int32u,
191                conversion: Some(KyoceraConversion::FNumber),
192            },
193        );
194
195        // MaxAperture at offset 0x68
196        // ExifTool: lines 80-82 - 0x68 => { Name => 'MaxAperture', Format => 'int32u', ValueConv => '2**($val / 16)', PrintConv => 'sprintf("%.1f",$val)' }
197        tag_definitions.insert(
198            0x68,
199            KyoceraTagDef {
200                name: "MaxAperture".to_string(),
201                offset: 0x68,
202                format: KyoceraFormat::Int32u,
203                conversion: Some(KyoceraConversion::MaxAperture),
204            },
205        );
206
207        // FocalLength at offset 0x70
208        // ExifTool: lines 83-84 - 0x70 => { Name => 'FocalLength', Format => 'int32u', PrintConv => '"$val mm"' }
209        tag_definitions.insert(
210            0x70,
211            KyoceraTagDef {
212                name: "FocalLength".to_string(),
213                offset: 0x70,
214                format: KyoceraFormat::Int32u,
215                conversion: Some(KyoceraConversion::FocalLength),
216            },
217        );
218
219        // Lens at offset 0x7c
220        // ExifTool: line 85 - 0x7c => { Name => 'Lens', Format => 'string[32]' }
221        tag_definitions.insert(
222            0x7c,
223            KyoceraTagDef {
224                name: "Lens".to_string(),
225                offset: 0x7c,
226                format: KyoceraFormat::String(32),
227                conversion: None,
228            },
229        );
230
231        Self { tag_definitions }
232    }
233
234    /// Extract value from binary data based on format
235    /// ExifTool: ProcessBinaryData value extraction logic
236    fn extract_value(
237        &self,
238        data: &[u8],
239        offset: usize,
240        format: &KyoceraFormat,
241    ) -> Result<TagValue> {
242        match format {
243            KyoceraFormat::String(length) => {
244                if offset + length > data.len() {
245                    return Err(ExifError::ParseError(format!(
246                        "String at offset {offset:#x} extends beyond data bounds"
247                    )));
248                }
249                let string_bytes = &data[offset..offset + length];
250                let string_value = String::from_utf8_lossy(string_bytes).to_string();
251                Ok(TagValue::String(string_value))
252            }
253            KyoceraFormat::Int32u => {
254                if offset + 4 > data.len() {
255                    return Err(ExifError::ParseError(format!(
256                        "Int32u at offset {offset:#x} extends beyond data bounds"
257                    )));
258                }
259                // Kyocera uses big-endian byte order (MM)
260                // ExifTool: KyoceraRaw.pm line 23 - ByteOrder => 'MM'
261                let value = u32::from_be_bytes([
262                    data[offset],
263                    data[offset + 1],
264                    data[offset + 2],
265                    data[offset + 3],
266                ]);
267                Ok(TagValue::U32(value))
268            }
269            KyoceraFormat::Int32uArray(count) => {
270                let total_bytes = count * 4;
271                if offset + total_bytes > data.len() {
272                    return Err(ExifError::ParseError(format!(
273                        "Int32u array at offset {offset:#x} extends beyond data bounds"
274                    )));
275                }
276
277                let mut values = Vec::new();
278                for i in 0..*count {
279                    let item_offset = offset + i * 4;
280                    let value = u32::from_be_bytes([
281                        data[item_offset],
282                        data[item_offset + 1],
283                        data[item_offset + 2],
284                        data[item_offset + 3],
285                    ]);
286                    values.push(value);
287                }
288                Ok(TagValue::U32Array(values))
289            }
290        }
291    }
292
293    /// Apply conversion to extracted value
294    /// ExifTool: ValueConv and PrintConv logic from tag definitions
295    fn apply_conversion(&self, value: &TagValue, conversion: &KyoceraConversion) -> TagValue {
296        match conversion {
297            KyoceraConversion::ReverseString => {
298                if let TagValue::String(s) = value {
299                    TagValue::String(utils::reverse_string(s.as_bytes()))
300                } else {
301                    value.clone()
302                }
303            }
304            KyoceraConversion::ExposureTime => {
305                if let TagValue::U32(val) = value {
306                    let exposure_time = utils::kyocera_exposure_time(*val);
307                    TagValue::F64(exposure_time)
308                } else {
309                    value.clone()
310                }
311            }
312            KyoceraConversion::FNumber | KyoceraConversion::MaxAperture => {
313                if let TagValue::U32(val) = value {
314                    let fnumber = utils::kyocera_fnumber(*val);
315                    TagValue::F64(fnumber)
316                } else {
317                    value.clone()
318                }
319            }
320            KyoceraConversion::DateTime => {
321                if let TagValue::String(s) = value {
322                    // First reverse the string, then format as datetime
323                    let reversed = utils::reverse_string(s.as_bytes());
324                    // ExifTool ConvertDateTime logic would go here
325                    // For now, just return the reversed string
326                    TagValue::String(reversed)
327                } else {
328                    value.clone()
329                }
330            }
331            KyoceraConversion::FocalLength => {
332                if let TagValue::U32(val) = value {
333                    TagValue::String(format!("{val} mm"))
334                } else {
335                    value.clone()
336                }
337            }
338            KyoceraConversion::IsoLookup => {
339                if let TagValue::U32(val) = value {
340                    if let Some(iso_speed) = utils::kyocera_iso_lookup(*val) {
341                        TagValue::U32(iso_speed)
342                    } else {
343                        value.clone() // Keep original value if not in lookup
344                    }
345                } else {
346                    value.clone()
347                }
348            }
349        }
350    }
351}
352
353impl RawFormatHandler for KyoceraRawHandler {
354    /// Process Kyocera RAW data
355    /// ExifTool: ProcessBinaryData call with KyoceraRaw::Main table
356    fn process_raw(&self, reader: &mut ExifReader, data: &[u8]) -> Result<()> {
357        // Validate minimum data size
358        // ExifTool: KyoceraRaw.pm processes exactly 156 bytes
359        if data.len() < 156 {
360            return Err(ExifError::ParseError(format!(
361                "Kyocera RAW data too short: {} bytes (expected 156)",
362                data.len()
363            )));
364        }
365
366        // Process each tag definition
367        for (&tag_id, tag_def) in &self.tag_definitions {
368            // Extract raw value
369            let raw_value = self.extract_value(data, tag_def.offset, &tag_def.format)?;
370
371            // Apply conversion if specified
372            let final_value = if let Some(conversion) = &tag_def.conversion {
373                self.apply_conversion(&raw_value, conversion)
374            } else {
375                raw_value
376            };
377
378            // Store the tag with source info
379            // ExifTool: Groups => { 0 => 'MakerNotes', 2 => 'Camera' }
380            // Individual tags may override group 2 (e.g., DateTimeOriginal => 'Time', ISO => 'Image')
381            let group2 = match tag_def.name.as_str() {
382                "DateTimeOriginal" => "Time",
383                "ISO" | "ExposureTime" | "FNumber" | "MaxAperture" | "FocalLength"
384                | "WB_RGGBLevels" => "Image",
385                _ => "Camera", // Default
386            };
387
388            let source_info = TagSourceInfo::new(
389                "MakerNotes".to_string(),
390                "KyoceraRaw".to_string(),
391                group2.to_string(),
392            );
393
394            reader.extracted_tags.insert(tag_id, final_value);
395            reader.tag_sources.insert(tag_id, source_info);
396        }
397
398        Ok(())
399    }
400
401    fn name(&self) -> &'static str {
402        "KyoceraRaw"
403    }
404
405    fn validate_format(&self, data: &[u8]) -> bool {
406        // ExifTool: KyoceraRaw.pm validation logic
407        // Check for minimum size and magic bytes
408        super::super::detector::validate_kyocera_magic(data)
409    }
410}
411
412/// Get Kyocera RAW tag name by ID
413/// ExifTool: lib/Image/ExifTool/KyoceraRaw.pm tag definitions
414pub fn get_kyocera_tag_name(tag_id: u16) -> Option<&'static str> {
415    match tag_id {
416        0x01 => Some("FirmwareVersion"),
417        0x0c => Some("Model"),
418        0x19 => Some("Make"),
419        0x21 => Some("DateTimeOriginal"),
420        0x34 => Some("ISO"),
421        0x38 => Some("ExposureTime"),
422        0x3c => Some("WB_RGGBLevels"),
423        0x58 => Some("FNumber"),
424        0x68 => Some("MaxAperture"),
425        0x70 => Some("FocalLength"),
426        0x7c => Some("Lens"),
427        _ => None,
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_kyocera_handler_creation() {
437        let handler = KyoceraRawHandler::new();
438        assert_eq!(handler.name(), "KyoceraRaw");
439
440        // Check that we have the expected number of tag definitions
441        assert_eq!(handler.tag_definitions.len(), 11);
442
443        // Check some key tag definitions
444        assert!(handler.tag_definitions.contains_key(&0x19)); // Make
445        assert!(handler.tag_definitions.contains_key(&0x34)); // ISO
446        assert!(handler.tag_definitions.contains_key(&0x38)); // ExposureTime
447    }
448
449    #[test]
450    fn test_get_kyocera_tag_name() {
451        // Test known tag names
452        assert_eq!(get_kyocera_tag_name(0x19), Some("Make"));
453        assert_eq!(get_kyocera_tag_name(0x34), Some("ISO"));
454        assert_eq!(get_kyocera_tag_name(0x38), Some("ExposureTime"));
455        assert_eq!(get_kyocera_tag_name(0x01), Some("FirmwareVersion"));
456        assert_eq!(get_kyocera_tag_name(0x0c), Some("Model"));
457
458        // Test unknown tag
459        assert_eq!(get_kyocera_tag_name(0x99), None);
460    }
461
462    #[test]
463    fn test_value_extraction() {
464        let handler = KyoceraRawHandler::new();
465
466        // Test string extraction
467        let mut data = vec![0u8; 20];
468        data[10..17].copy_from_slice(b"ARECOYK");
469
470        let result = handler
471            .extract_value(&data, 10, &KyoceraFormat::String(7))
472            .unwrap();
473        if let TagValue::String(s) = result {
474            assert!(s.starts_with("ARECOYK"));
475        } else {
476            panic!("Expected String value");
477        }
478
479        // Test int32u extraction (big-endian)
480        let mut data = vec![0u8; 10];
481        data[4..8].copy_from_slice(&[0x00, 0x00, 0x01, 0x00]); // 256 in big-endian
482
483        let result = handler
484            .extract_value(&data, 4, &KyoceraFormat::Int32u)
485            .unwrap();
486        if let TagValue::U32(val) = result {
487            assert_eq!(val, 256);
488        } else {
489            panic!("Expected U32 value");
490        }
491    }
492
493    #[test]
494    fn test_conversions() {
495        let handler = KyoceraRawHandler::new();
496
497        // Test string reversal
498        let input = TagValue::String("KYOCERA\0".to_string());
499        let result = handler.apply_conversion(&input, &KyoceraConversion::ReverseString);
500        if let TagValue::String(s) = result {
501            assert!(s.starts_with("ARECOYK"));
502        } else {
503            panic!("Expected String value");
504        }
505
506        // Test ISO lookup
507        let input = TagValue::U32(13); // Should map to ISO 100
508        let result = handler.apply_conversion(&input, &KyoceraConversion::IsoLookup);
509        if let TagValue::U32(iso) = result {
510            assert_eq!(iso, 100);
511        } else {
512            panic!("Expected U32 value");
513        }
514
515        // Test focal length formatting
516        let input = TagValue::U32(50);
517        let result = handler.apply_conversion(&input, &KyoceraConversion::FocalLength);
518        if let TagValue::String(s) = result {
519            assert_eq!(s, "50 mm");
520        } else {
521            panic!("Expected String value");
522        }
523    }
524
525    #[test]
526    fn test_format_validation() {
527        let handler = KyoceraRawHandler::new();
528
529        // Create valid test data with magic bytes
530        let mut valid_data = vec![0u8; 156];
531        valid_data[0x19..0x19 + 7].copy_from_slice(b"ARECOYK");
532
533        assert!(handler.validate_format(&valid_data));
534
535        // Test invalid data
536        let invalid_data = vec![0u8; 100]; // Too short
537        assert!(!handler.validate_format(&invalid_data));
538
539        let mut wrong_magic = vec![0u8; 156];
540        wrong_magic[0x19..0x19 + 7].copy_from_slice(b"WRONGXY");
541        assert!(!handler.validate_format(&wrong_magic));
542    }
543}