Skip to main content

exif_oxide/raw/formats/
panasonic.rs

1//! Panasonic RAW format handler
2
3#![allow(dead_code, unused_variables)]
4//!
5//! This module implements ExifTool's PanasonicRaw.pm processing logic exactly.
6//! Panasonic RW2/RWL files are TIFF-based formats with manufacturer-specific tags
7//! and entry-based offset handling for some data blocks.
8//!
9//! ExifTool Reference: lib/Image/ExifTool/PanasonicRaw.pm (956 lines total)
10//! Processing: Standard TIFF IFD processing with specialized entry-based offsets
11
12use crate::exif::ExifReader;
13use crate::raw::offset::{
14    EntryBasedOffsetProcessor, OffsetBase, OffsetExtractionRule, OffsetField,
15};
16use crate::raw::RawFormatHandler;
17use crate::tiff_types::TiffHeader;
18use crate::types::{DirectoryInfo, ExifError, Result, TagSourceInfo, TagValue};
19use std::collections::HashMap;
20use tracing;
21
22/// Panasonic RAW format handler
23/// ExifTool: lib/Image/ExifTool/PanasonicRaw.pm - TIFF-based with entry-based offsets
24pub struct PanasonicRawHandler {
25    /// Main binary data processor for standard Panasonic tags
26    /// ExifTool: %Image::ExifTool::PanasonicRaw::Main hash (lines 70-380)
27    binary_processor: PanasonicBinaryProcessor,
28
29    /// Entry-based offset processor for special data blocks
30    /// ExifTool: Tags like JpgFromRaw (0x002e) use entry-based offsets
31    offset_processor: EntryBasedOffsetProcessor,
32}
33
34/// Binary data processor for Panasonic main tags
35/// ExifTool: %Image::ExifTool::PanasonicRaw::Main hash (lines 70-380)
36struct PanasonicBinaryProcessor {
37    /// Tag definitions for main Panasonic IFD
38    tag_definitions: HashMap<u16, PanasonicTagDef>,
39}
40
41/// Panasonic tag definition
42/// ExifTool: Individual entries in %Image::ExifTool::PanasonicRaw::Main
43#[derive(Debug, Clone)]
44struct PanasonicTagDef {
45    /// Tag name
46    /// ExifTool: Name field in tag hash
47    name: String,
48
49    /// Data format
50    /// ExifTool: Format field or inferred from type
51    format: PanasonicFormat,
52
53    /// Value conversion
54    /// ExifTool: ValueConv field
55    value_conv: Option<PanasonicValueConv>,
56
57    /// Print conversion
58    /// ExifTool: PrintConv field
59    print_conv: Option<PanasonicPrintConv>,
60
61    /// Groups assignment
62    /// ExifTool: Groups field override
63    groups: Option<PanasonicGroups>,
64
65    /// Writability
66    /// ExifTool: Writable field
67    writable: bool,
68}
69
70/// Panasonic data formats
71/// ExifTool: Format specifications in tag definitions
72#[derive(Debug, Clone)]
73enum PanasonicFormat {
74    /// Undefined binary data
75    /// ExifTool: undef format
76    Undef,
77
78    /// 16-bit unsigned integer
79    /// ExifTool: int16u format
80    Int16u,
81
82    /// 32-bit unsigned integer
83    /// ExifTool: int32u format
84    Int32u,
85
86    /// String data
87    /// ExifTool: string format
88    String,
89
90    /// Array of int16u values
91    /// ExifTool: int16u[N] format
92    Int16uArray(usize),
93}
94
95/// Panasonic value conversion types
96/// ExifTool: ValueConv fields in tag definitions
97#[derive(Debug, Clone)]
98enum PanasonicValueConv {
99    /// Division by constant
100    /// ExifTool: '$val / 256' pattern
101    DivideBy256,
102
103    /// Multiplication by constant
104    /// ExifTool: '$val * 100' pattern
105    MultiplyBy100,
106}
107
108/// Panasonic print conversion types
109/// ExifTool: PrintConv fields in tag definitions
110#[derive(Debug, Clone)]
111enum PanasonicPrintConv {
112    /// CFA pattern lookup
113    /// ExifTool: lines 96-102 - CFA pattern hash
114    CfaPattern,
115
116    /// Compression type lookup
117    /// ExifTool: lines 109-114 - Compression hash
118    Compression,
119
120    /// Orientation lookup (reuses EXIF orientation)
121    /// ExifTool: line 250 - \%Image::ExifTool::Exif::orientation
122    Orientation,
123
124    /// PrintConv hash lookup
125    /// ExifTool: Static hash with key-value mappings
126    HashLookup(HashMap<u32, String>),
127}
128
129/// Panasonic group assignments
130/// ExifTool: Groups field overrides default groups
131#[derive(Debug, Clone)]
132struct PanasonicGroups {
133    /// Group 0 (family)
134    /// ExifTool: Groups => { 0 => '...' }
135    group0: Option<String>,
136
137    /// Group 1 (location)
138    /// ExifTool: Groups => { 1 => '...' }
139    group1: Option<String>,
140
141    /// Group 2 (category)
142    /// ExifTool: Groups => { 2 => '...' }
143    group2: Option<String>,
144}
145
146impl Default for PanasonicRawHandler {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152impl PanasonicRawHandler {
153    /// Process entry-based offsets for Panasonic-specific data blocks
154    /// ExifTool: PanasonicRaw.pm applies additional processing for tags like JpgFromRaw (0x002e)
155    fn process_entry_based_offsets(&self, reader: &mut ExifReader, data: &[u8]) -> Result<()> {
156        // Get the currently extracted tags from TIFF IFD processing
157        // Clone the tags to avoid borrowing issues
158        let extracted_tags: Vec<(u16, TagValue)> = reader
159            .get_extracted_tags()
160            .iter()
161            .map(|(tag_id, tag_value)| (*tag_id, tag_value.clone()))
162            .collect();
163
164        // Process each tag that has entry-based offset rules
165        for (tag_id, tag_value) in extracted_tags {
166            if let Some(rule) = self.offset_processor.get_rule(tag_id) {
167                // Extract offset from the tag value based on the rule
168                let offset = match rule.offset_field {
169                    OffsetField::ActualValue => {
170                        // The tag value itself is the offset
171                        match tag_value.as_u32() {
172                            Some(val) => val as u64,
173                            None => continue, // Skip if not convertible to u32
174                        }
175                    }
176                    OffsetField::ValueOffset => {
177                        // This would be for more complex offset extraction
178                        // For now, treat as actual value
179                        match tag_value.as_u32() {
180                            Some(val) => val as u64,
181                            None => continue,
182                        }
183                    }
184                };
185
186                // Calculate the final offset based on the rule's base
187                let final_offset = match rule.base {
188                    OffsetBase::FileStart => offset + rule.additional_offset as u64,
189                    OffsetBase::IfdStart => {
190                        // Would need IFD start position - for now use file start
191                        offset + rule.additional_offset as u64
192                    }
193                    OffsetBase::MakerNoteStart => {
194                        // Would need maker note start - for now use file start
195                        offset + rule.additional_offset as u64
196                    }
197                    OffsetBase::DataPosition => offset + rule.additional_offset as u64,
198                };
199
200                // Extract data at the calculated offset
201                // ExifTool: PanasonicRaw.pm extracts additional data blocks at these offsets
202                if let Some(extracted_data) =
203                    self.extract_data_at_offset(data, final_offset, tag_id)
204                {
205                    // Process the extracted data based on the tag type
206                    self.process_offset_data(reader, tag_id, &extracted_data)?;
207                }
208            }
209        }
210
211        Ok(())
212    }
213
214    /// Extract data at a specific offset for entry-based processing
215    /// ExifTool: PanasonicRaw.pm extracts data blocks at calculated offsets
216    fn extract_data_at_offset(&self, data: &[u8], offset: u64, tag_id: u16) -> Option<Vec<u8>> {
217        let offset = offset as usize;
218
219        // Bounds check
220        if offset >= data.len() {
221            return None;
222        }
223
224        // Determine data size based on tag type
225        // ExifTool: Different tags extract different amounts of data
226        let size = match tag_id {
227            0x002e => 4, // JpgFromRaw - typically a 4-byte offset
228            0x0127 => 4, // JpgFromRaw2 - typically a 4-byte offset
229            _ => 4,      // Default size for unknown entry-based tags
230        };
231
232        // Extract the data block
233        if offset + size <= data.len() {
234            Some(data[offset..offset + size].to_vec())
235        } else {
236            // Extract remaining data if partial block available
237            Some(data[offset..].to_vec())
238        }
239    }
240
241    /// Process data extracted at calculated offsets
242    /// ExifTool: PanasonicRaw.pm processes different types of offset data
243    fn process_offset_data(
244        &self,
245        _reader: &mut ExifReader,
246        tag_id: u16,
247        data: &[u8],
248    ) -> Result<()> {
249        // Process based on the specific tag type
250        match tag_id {
251            0x002e => {
252                // JpgFromRaw - points to embedded JPEG data
253                // For now, just note that we found the offset
254                if data.len() >= 4 {
255                    let jpeg_offset = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
256                    // Could extract JPEG metadata here in the future
257                    // ExifTool: PanasonicRaw.pm extracts embedded JPEG EXIF data
258                    tracing::debug!("Found JpgFromRaw offset: {:#x}", jpeg_offset);
259                }
260            }
261            0x0127 => {
262                // JpgFromRaw2 - alternate JPEG data pointer
263                if data.len() >= 4 {
264                    let jpeg_offset = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
265                    tracing::debug!("Found JpgFromRaw2 offset: {:#x}", jpeg_offset);
266                }
267            }
268            _ => {
269                // Unknown entry-based tag - log for debugging
270                tracing::debug!(
271                    "Processed entry-based offset for tag {:#x}: {} bytes",
272                    tag_id,
273                    data.len()
274                );
275            }
276        }
277
278        Ok(())
279    }
280    /// Create new Panasonic RAW handler with processors
281    /// ExifTool: %Image::ExifTool::PanasonicRaw::Main hash construction
282    pub fn new() -> Self {
283        Self {
284            binary_processor: PanasonicBinaryProcessor::new(),
285            offset_processor: Self::create_offset_processor(),
286        }
287    }
288
289    /// Create entry-based offset processor with Panasonic rules
290    /// ExifTool: PanasonicRaw.pm entry-based offset handling
291    fn create_offset_processor() -> EntryBasedOffsetProcessor {
292        let mut offset_rules = HashMap::new();
293
294        // JpgFromRaw tag at 0x002e uses entry-based offset
295        // ExifTool: line 198 - 0x2e => { Name => 'JpgFromRaw', ... }
296        // The offset is stored in the IFD entry value and points to JPEG data
297        offset_rules.insert(
298            0x002e,
299            OffsetExtractionRule {
300                tag_id: 0x002e,
301                offset_field: OffsetField::ActualValue,
302                base: OffsetBase::FileStart,
303                additional_offset: 0,
304            },
305        );
306
307        // JpgFromRaw2 tag at 0x0127 (newer models)
308        // ExifTool: line 301 - 0x127 => { Name => 'JpgFromRaw2', ... }
309        offset_rules.insert(
310            0x0127,
311            OffsetExtractionRule {
312                tag_id: 0x0127,
313                offset_field: OffsetField::ActualValue,
314                base: OffsetBase::FileStart,
315                additional_offset: 0,
316            },
317        );
318
319        EntryBasedOffsetProcessor::new(offset_rules)
320    }
321}
322
323impl RawFormatHandler for PanasonicRawHandler {
324    /// Process Panasonic RW2/RWL data
325    /// ExifTool: Standard TIFF processing with Panasonic-specific handling
326    fn process_raw(&self, reader: &mut ExifReader, data: &[u8]) -> Result<()> {
327        // Panasonic RW2/RWL files are TIFF-based, so we parse the TIFF header first
328        // ExifTool: lib/Image/ExifTool/PanasonicRaw.pm uses standard TIFF processing
329
330        // Step 1: Parse TIFF header to get IFD structure
331        let header = TiffHeader::parse(data)?;
332        reader.set_test_header(header.clone());
333        reader.set_test_data(data.to_vec());
334
335        // Step 2: Process the main IFD using existing TIFF infrastructure
336        // ExifTool: PanasonicRaw.pm processes TIFF IFDs first, then applies special handling
337        let dir_info = DirectoryInfo {
338            name: "PanasonicRaw".to_string(),
339            dir_start: header.ifd0_offset as usize,
340            dir_len: 0,  // Will be calculated by IFD processing
341            base: 0,     // Standard TIFF base
342            data_pos: 0, // No additional data position offset
343            allow_reprocess: false,
344        };
345
346        // Process the TIFF IFD to extract real tag entries
347        reader.process_subdirectory(&dir_info)?;
348
349        // Step 3: Apply entry-based offset processing to extracted IFD entries
350        // ExifTool: PanasonicRaw.pm applies additional processing for specific tags like JpgFromRaw
351        self.process_entry_based_offsets(reader, data)?;
352
353        Ok(())
354    }
355
356    fn name(&self) -> &'static str {
357        "PanasonicRaw"
358    }
359
360    fn validate_format(&self, data: &[u8]) -> bool {
361        // ExifTool: PanasonicRaw.pm validation logic - TIFF-based format
362        super::super::detector::validate_panasonic_rw2_magic(data)
363    }
364}
365
366impl PanasonicBinaryProcessor {
367    /// Create new Panasonic binary processor with tag definitions
368    /// ExifTool: %Image::ExifTool::PanasonicRaw::Main hash (lines 70-380)
369    fn new() -> Self {
370        let mut tag_definitions = HashMap::new();
371
372        // PanasonicRawVersion at 0x01
373        // ExifTool: line 76 - 0x01 => { Name => 'PanasonicRawVersion', Writable => 'undef' }
374        tag_definitions.insert(
375            0x01,
376            PanasonicTagDef {
377                name: "PanasonicRawVersion".to_string(),
378                format: PanasonicFormat::Undef,
379                value_conv: None,
380                print_conv: None,
381                groups: None,
382                writable: true,
383            },
384        );
385
386        // SensorWidth at 0x02
387        // ExifTool: line 80 - 0x02 => 'SensorWidth'
388        tag_definitions.insert(
389            0x02,
390            PanasonicTagDef {
391                name: "SensorWidth".to_string(),
392                format: PanasonicFormat::Int16u,
393                value_conv: None,
394                print_conv: None,
395                groups: None,
396                writable: false,
397            },
398        );
399
400        // SensorHeight at 0x03
401        // ExifTool: line 81 - 0x03 => 'SensorHeight'
402        tag_definitions.insert(
403            0x03,
404            PanasonicTagDef {
405                name: "SensorHeight".to_string(),
406                format: PanasonicFormat::Int16u,
407                value_conv: None,
408                print_conv: None,
409                groups: None,
410                writable: false,
411            },
412        );
413
414        // SensorTopBorder at 0x04
415        // ExifTool: line 82 - 0x04 => 'SensorTopBorder'
416        tag_definitions.insert(
417            0x04,
418            PanasonicTagDef {
419                name: "SensorTopBorder".to_string(),
420                format: PanasonicFormat::Int16u,
421                value_conv: None,
422                print_conv: None,
423                groups: None,
424                writable: false,
425            },
426        );
427
428        // SensorLeftBorder at 0x05
429        // ExifTool: line 83 - 0x05 => 'SensorLeftBorder'
430        tag_definitions.insert(
431            0x05,
432            PanasonicTagDef {
433                name: "SensorLeftBorder".to_string(),
434                format: PanasonicFormat::Int16u,
435                value_conv: None,
436                print_conv: None,
437                groups: None,
438                writable: false,
439            },
440        );
441
442        // SensorBottomBorder at 0x06
443        // ExifTool: line 84 - 0x06 => 'SensorBottomBorder'
444        tag_definitions.insert(
445            0x06,
446            PanasonicTagDef {
447                name: "SensorBottomBorder".to_string(),
448                format: PanasonicFormat::Int16u,
449                value_conv: None,
450                print_conv: None,
451                groups: None,
452                writable: false,
453            },
454        );
455
456        // SensorRightBorder at 0x07
457        // ExifTool: line 85 - 0x07 => 'SensorRightBorder'
458        tag_definitions.insert(
459            0x07,
460            PanasonicTagDef {
461                name: "SensorRightBorder".to_string(),
462                format: PanasonicFormat::Int16u,
463                value_conv: None,
464                print_conv: None,
465                groups: None,
466                writable: false,
467            },
468        );
469
470        // CFAPattern at 0x09
471        // ExifTool: line 92 - 0x09 => { Name => 'CFAPattern', ... PrintConv => {...} }
472        let mut cfa_pattern_lookup = HashMap::new();
473        cfa_pattern_lookup.insert(0, "n/a".to_string());
474        cfa_pattern_lookup.insert(1, "[Red,Green][Green,Blue]".to_string());
475        cfa_pattern_lookup.insert(2, "[Green,Red][Blue,Green]".to_string());
476        cfa_pattern_lookup.insert(3, "[Green,Blue][Red,Green]".to_string());
477        cfa_pattern_lookup.insert(4, "[Blue,Green][Green,Red]".to_string());
478
479        tag_definitions.insert(
480            0x09,
481            PanasonicTagDef {
482                name: "CFAPattern".to_string(),
483                format: PanasonicFormat::Int16u,
484                value_conv: None,
485                print_conv: Some(PanasonicPrintConv::HashLookup(cfa_pattern_lookup)),
486                groups: None,
487                writable: true,
488            },
489        );
490
491        // BitsPerSample at 0x0a
492        // ExifTool: line 104 - 0x0a => { Name => 'BitsPerSample', Writable => 'int16u', Protected => 1 }
493        tag_definitions.insert(
494            0x0a,
495            PanasonicTagDef {
496                name: "BitsPerSample".to_string(),
497                format: PanasonicFormat::Int16u,
498                value_conv: None,
499                print_conv: None,
500                groups: None,
501                writable: true,
502            },
503        );
504
505        // Compression at 0x0b
506        // ExifTool: line 105 - 0x0b => { Name => 'Compression', ... PrintConv => {...} }
507        let mut compression_lookup = HashMap::new();
508        compression_lookup.insert(34316, "Panasonic RAW 1".to_string());
509        compression_lookup.insert(34826, "Panasonic RAW 2".to_string());
510        compression_lookup.insert(34828, "Panasonic RAW 3".to_string());
511        compression_lookup.insert(34830, "Panasonic RAW 4".to_string());
512
513        tag_definitions.insert(
514            0x0b,
515            PanasonicTagDef {
516                name: "Compression".to_string(),
517                format: PanasonicFormat::Int16u,
518                value_conv: None,
519                print_conv: Some(PanasonicPrintConv::HashLookup(compression_lookup)),
520                groups: None,
521                writable: true,
522            },
523        );
524
525        // RedBalance at 0x11
526        // ExifTool: line 122 - 0x11 => { Name => 'RedBalance', Writable => 'int16u', ValueConv => '$val / 256', ... }
527        tag_definitions.insert(
528            0x11,
529            PanasonicTagDef {
530                name: "RedBalance".to_string(),
531                format: PanasonicFormat::Int16u,
532                value_conv: Some(PanasonicValueConv::DivideBy256),
533                print_conv: None,
534                groups: None,
535                writable: true,
536            },
537        );
538
539        // BlueBalance at 0x12
540        // ExifTool: line 129 - 0x12 => { Name => 'BlueBalance', Writable => 'int16u', ValueConv => '$val / 256', ... }
541        tag_definitions.insert(
542            0x12,
543            PanasonicTagDef {
544                name: "BlueBalance".to_string(),
545                format: PanasonicFormat::Int16u,
546                value_conv: Some(PanasonicValueConv::DivideBy256),
547                print_conv: None,
548                groups: None,
549                writable: true,
550            },
551        );
552
553        // ISO at 0x17
554        // ExifTool: line 139 - 0x17 => { Name => 'ISO', Writable => 'int16u' }
555        tag_definitions.insert(
556            0x17,
557            PanasonicTagDef {
558                name: "ISO".to_string(),
559                format: PanasonicFormat::Int16u,
560                value_conv: None,
561                print_conv: None,
562                groups: None,
563                writable: true,
564            },
565        );
566
567        Self { tag_definitions }
568    }
569
570    /// Process Panasonic binary data
571    /// ExifTool: ProcessBinaryData equivalent for Panasonic tags
572    fn process(&self, reader: &mut ExifReader, data: &[u8]) -> Result<()> {
573        // Since RW2/RWL are TIFF-based, we need proper TIFF IFD processing
574        // For now, we'll extract what we can from the beginning of the data
575        // TODO: Implement proper TIFF IFD parsing integration
576
577        // For basic demonstration, process a few tags if data is available
578        if data.len() < 20 {
579            return Ok(()); // Not enough data for meaningful processing
580        }
581
582        // This is a simplified placeholder - real implementation would:
583        // 1. Parse TIFF header to determine byte order
584        // 2. Read IFD entries
585        // 3. Extract values based on tag definitions
586        // 4. Apply value/print conversions
587
588        // Store a basic tag to show the handler is working
589        let source_info =
590            TagSourceInfo::new("EXIF".to_string(), "IFD0".to_string(), "Image".to_string());
591
592        // Placeholder: create a basic tag entry
593        reader.extracted_tags.insert(
594            0x0001, // PanasonicRawVersion equivalent
595            TagValue::String("Panasonic RW2/RWL".to_string()),
596        );
597        reader.tag_sources.insert(0x0001, source_info);
598
599        Ok(())
600    }
601
602    /// Extract value from data based on format
603    /// ExifTool: Format-specific value extraction
604    #[allow(dead_code)]
605    fn extract_value(
606        &self,
607        data: &[u8],
608        offset: usize,
609        format: &PanasonicFormat,
610        byte_order: ByteOrder,
611    ) -> Result<TagValue> {
612        match format {
613            PanasonicFormat::Int16u => {
614                if offset + 2 > data.len() {
615                    return Err(ExifError::ParseError(format!(
616                        "Int16u at offset {offset:#x} extends beyond data bounds"
617                    )));
618                }
619                let value = match byte_order {
620                    ByteOrder::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
621                    ByteOrder::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
622                };
623                Ok(TagValue::U16(value))
624            }
625            PanasonicFormat::Int32u => {
626                if offset + 4 > data.len() {
627                    return Err(ExifError::ParseError(format!(
628                        "Int32u at offset {offset:#x} extends beyond data bounds"
629                    )));
630                }
631                let value = match byte_order {
632                    ByteOrder::BigEndian => u32::from_be_bytes([
633                        data[offset],
634                        data[offset + 1],
635                        data[offset + 2],
636                        data[offset + 3],
637                    ]),
638                    ByteOrder::LittleEndian => u32::from_le_bytes([
639                        data[offset],
640                        data[offset + 1],
641                        data[offset + 2],
642                        data[offset + 3],
643                    ]),
644                };
645                Ok(TagValue::U32(value))
646            }
647            PanasonicFormat::String => {
648                // For string, we'd need to know the length or find null terminator
649                // This is a simplified implementation
650                let mut end = offset;
651                while end < data.len() && data[end] != 0 {
652                    end += 1;
653                }
654                let string_value = String::from_utf8_lossy(&data[offset..end]).to_string();
655                Ok(TagValue::String(string_value))
656            }
657            PanasonicFormat::Undef => {
658                // Return as binary data
659                Ok(TagValue::Binary(data[offset..].to_vec()))
660            }
661            PanasonicFormat::Int16uArray(count) => {
662                let total_bytes = count * 2;
663                if offset + total_bytes > data.len() {
664                    return Err(ExifError::ParseError(format!(
665                        "Int16u array at offset {offset:#x} extends beyond data bounds"
666                    )));
667                }
668
669                let mut values = Vec::new();
670                for i in 0..*count {
671                    let item_offset = offset + i * 2;
672                    let value = match byte_order {
673                        ByteOrder::BigEndian => {
674                            u16::from_be_bytes([data[item_offset], data[item_offset + 1]])
675                        }
676                        ByteOrder::LittleEndian => {
677                            u16::from_le_bytes([data[item_offset], data[item_offset + 1]])
678                        }
679                    };
680                    values.push(value);
681                }
682                Ok(TagValue::U16Array(values))
683            }
684        }
685    }
686
687    /// Apply value conversion to extracted value
688    /// ExifTool: ValueConv logic from tag definitions
689    #[allow(dead_code)]
690    fn apply_value_conversion(
691        &self,
692        value: &TagValue,
693        conversion: &PanasonicValueConv,
694    ) -> TagValue {
695        match conversion {
696            PanasonicValueConv::DivideBy256 => match value {
697                TagValue::U16(val) => TagValue::F64(*val as f64 / 256.0),
698                TagValue::U32(val) => TagValue::F64(*val as f64 / 256.0),
699                _ => value.clone(),
700            },
701            PanasonicValueConv::MultiplyBy100 => match value {
702                TagValue::U16(val) => TagValue::U32(*val as u32 * 100),
703                TagValue::U32(val) => TagValue::U32(*val * 100),
704                _ => value.clone(),
705            },
706        }
707    }
708
709    /// Apply print conversion to value
710    /// ExifTool: PrintConv logic from tag definitions
711    #[allow(dead_code)]
712    fn apply_print_conversion(
713        &self,
714        value: &TagValue,
715        conversion: &PanasonicPrintConv,
716    ) -> TagValue {
717        match conversion {
718            PanasonicPrintConv::HashLookup(lookup) => {
719                if let Some(key) = value.as_u16().map(|v| v as u32).or_else(|| value.as_u32()) {
720                    if let Some(description) = lookup.get(&key) {
721                        return TagValue::String(description.clone());
722                    }
723                }
724                value.clone()
725            }
726            PanasonicPrintConv::CfaPattern => {
727                // Handled by HashLookup in practice
728                value.clone()
729            }
730            PanasonicPrintConv::Compression => {
731                // Handled by HashLookup in practice
732                value.clone()
733            }
734            PanasonicPrintConv::Orientation => {
735                // Would use EXIF orientation lookup
736                value.clone()
737            }
738        }
739    }
740}
741
742/// Byte order for data processing
743/// ExifTool: TIFF byte order from header
744#[derive(Debug, Clone, Copy)]
745enum ByteOrder {
746    /// Big-endian (MM)
747    BigEndian,
748    /// Little-endian (II)
749    LittleEndian,
750}
751
752/// Get Panasonic RW2 tag name by ID
753/// ExifTool: lib/Image/ExifTool/PanasonicRaw.pm tag definitions
754pub fn get_panasonic_tag_name(tag_id: u16) -> Option<&'static str> {
755    match tag_id {
756        0x01 => Some("PanasonicRawVersion"),
757        0x02 => Some("SensorWidth"),
758        0x03 => Some("SensorHeight"),
759        0x04 => Some("SensorTopBorder"),
760        0x05 => Some("SensorLeftBorder"),
761        0x06 => Some("SensorBottomBorder"),
762        0x07 => Some("SensorRightBorder"),
763        0x08 => Some("SamplesPerPixel"),
764        0x09 => Some("CFAPattern"),
765        0x0a => Some("BitsPerSample"),
766        0x0b => Some("Compression"),
767        0x11 => Some("RedBalance"),
768        0x12 => Some("BlueBalance"),
769        0x17 => Some("ISO"),
770        0x002e => Some("JpgFromRaw"),
771        0x0127 => Some("JpgFromRaw2"),
772        _ => None,
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779
780    #[test]
781    fn test_panasonic_handler_creation() {
782        let handler = PanasonicRawHandler::new();
783        assert_eq!(handler.name(), "PanasonicRaw");
784    }
785
786    #[test]
787    fn test_get_panasonic_tag_name() {
788        // Test known tag names
789        assert_eq!(get_panasonic_tag_name(0x01), Some("PanasonicRawVersion"));
790        assert_eq!(get_panasonic_tag_name(0x02), Some("SensorWidth"));
791        assert_eq!(get_panasonic_tag_name(0x09), Some("CFAPattern"));
792        assert_eq!(get_panasonic_tag_name(0x17), Some("ISO"));
793        assert_eq!(get_panasonic_tag_name(0x002e), Some("JpgFromRaw"));
794
795        // Test unknown tag
796        assert_eq!(get_panasonic_tag_name(0x9999), None);
797    }
798
799    #[test]
800    fn test_format_validation() {
801        let handler = PanasonicRawHandler::new();
802
803        // Test valid TIFF magic (big-endian)
804        let valid_be_data = b"MM\x00\x2A\x00\x00\x00\x08test_data";
805        assert!(handler.validate_format(valid_be_data));
806
807        // Test valid TIFF magic (little-endian)
808        let valid_le_data = b"II\x2A\x00\x08\x00\x00\x00test_data";
809        assert!(handler.validate_format(valid_le_data));
810
811        // Test invalid magic
812        let invalid_data = b"XX\x2A\x00\x08\x00\x00\x00test_data";
813        assert!(!handler.validate_format(invalid_data));
814
815        // Test insufficient data
816        let short_data = b"MM\x00";
817        assert!(!handler.validate_format(short_data));
818    }
819
820    #[test]
821    fn test_entry_based_offset_rules() {
822        let handler = PanasonicRawHandler::new();
823
824        // Test that entry-based offset processor has the expected rules
825        let configured_tags = handler.offset_processor.get_configured_tags();
826        assert!(configured_tags.contains(&0x002e)); // JpgFromRaw
827        assert!(configured_tags.contains(&0x0127)); // JpgFromRaw2
828
829        // Test rule retrieval
830        let rule_002e = handler.offset_processor.get_rule(0x002e);
831        assert!(rule_002e.is_some());
832        let rule = rule_002e.unwrap();
833        assert_eq!(rule.tag_id, 0x002e);
834        assert!(matches!(rule.offset_field, OffsetField::ActualValue));
835        assert!(matches!(rule.base, OffsetBase::FileStart));
836    }
837
838    #[test]
839    fn test_binary_processor_creation() {
840        let processor = PanasonicBinaryProcessor::new();
841
842        // Test that processor has expected tag definitions
843        assert!(processor.tag_definitions.contains_key(&0x01)); // PanasonicRawVersion
844        assert!(processor.tag_definitions.contains_key(&0x02)); // SensorWidth
845        assert!(processor.tag_definitions.contains_key(&0x09)); // CFAPattern
846        assert!(processor.tag_definitions.contains_key(&0x17)); // ISO
847
848        // Test tag definition details
849        let sensor_width_def = &processor.tag_definitions[&0x02];
850        assert_eq!(sensor_width_def.name, "SensorWidth");
851        assert!(matches!(sensor_width_def.format, PanasonicFormat::Int16u));
852        assert!(!sensor_width_def.writable);
853
854        let cfa_pattern_def = &processor.tag_definitions[&0x09];
855        assert_eq!(cfa_pattern_def.name, "CFAPattern");
856        assert!(cfa_pattern_def.print_conv.is_some());
857        assert!(cfa_pattern_def.writable);
858    }
859}