Skip to main content

exif_oxide/raw/formats/
minolta.rs

1//! Minolta RAW format handler
2
3#![allow(dead_code, unused_variables)]
4//!
5//! This module implements ExifTool's MinoltaRaw.pm processing logic exactly.
6//! Minolta/Konica-Minolta MRW files use a multi-block structure with separate
7//! sections for TIFF tags (TTW), picture raw data info (PRD), white balance (WBG),
8//! and requested image format (RIF).
9//!
10//! ExifTool Reference: lib/Image/ExifTool/MinoltaRaw.pm (537 lines total)
11//! Processing: ProcessMRW with multiple block types and TIFF subdirectories
12
13use crate::exif::ExifReader;
14use crate::raw::RawFormatHandler;
15use crate::types::{ExifError, Result, TagSourceInfo, TagValue};
16use std::collections::HashMap;
17
18/// Minolta RAW format handler
19/// ExifTool: lib/Image/ExifTool/MinoltaRaw.pm - Multi-block structure with ProcessMRW
20pub struct MinoltaRawHandler {
21    /// Processor for PRD (Picture Raw Data) blocks
22    /// ExifTool: %Image::ExifTool::MinoltaRaw::PRD hash (lines 56-110)
23    prd_processor: MinoltaPrdProcessor,
24
25    /// Processor for WBG (White Balance Gains) blocks
26    /// ExifTool: %Image::ExifTool::MinoltaRaw::WBG hash (lines 113-137)
27    wbg_processor: MinoltaWbgProcessor,
28
29    /// Processor for RIF (Requested Image Format) blocks
30    /// ExifTool: %Image::ExifTool::MinoltaRaw::RIF hash (lines 140-346)
31    rif_processor: MinoltaRifProcessor,
32}
33
34/// MRW file header structure
35/// ExifTool: MinoltaRaw.pm ProcessMRW function lines 392-494
36#[derive(Debug)]
37struct MrwHeader {
38    /// Byte order - 'M' for big-endian, 'I' for little-endian
39    /// ExifTool: MinoltaRaw.pm line 412 - SetByteOrder($1 . $1)
40    byte_order: ByteOrder,
41
42    /// Offset to image data
43    /// ExifTool: MinoltaRaw.pm line 420 - my $offset = Get32u(\$data, 4) + $pos
44    data_offset: u32,
45
46    /// MRW blocks found in the file
47    /// ExifTool: MinoltaRaw.pm lines 424-476 - loop through segments
48    blocks: Vec<MrwBlock>,
49}
50
51/// Individual MRW block
52/// ExifTool: MinoltaRaw.pm lines 427-476 - segment processing
53#[derive(Debug)]
54struct MrwBlock {
55    /// Block type tag (4 bytes)
56    /// ExifTool: MinoltaRaw.pm line 427 - my $tag = substr($data, 0, 4)
57    tag: [u8; 4],
58
59    /// Block data length
60    /// ExifTool: MinoltaRaw.pm line 428 - my $len = Get32u(\$data, 4)
61    length: u32,
62
63    /// Block data
64    /// ExifTool: MinoltaRaw.pm line 442 - $raf->Read($buff, $len)
65    data: Vec<u8>,
66}
67
68/// Byte order for MRW processing
69/// ExifTool: MinoltaRaw.pm line 412 - checks MRM vs MRI
70#[derive(Debug, Clone, Copy)]
71enum ByteOrder {
72    /// Big-endian (MRM)
73    /// ExifTool: MinoltaRaw.pm line 408 - "\0MRM" for big-endian
74    BigEndian,
75
76    /// Little-endian (MRI)
77    /// ExifTool: MinoltaRaw.pm line 409 - "\0MRI" for little-endian (ARW images)
78    LittleEndian,
79}
80
81/// PRD (Picture Raw Data) processor
82/// ExifTool: %Image::ExifTool::MinoltaRaw::PRD hash (lines 56-110)
83struct MinoltaPrdProcessor {
84    /// Tag definitions for PRD block
85    tag_definitions: HashMap<u8, PrdTagDef>,
86}
87
88/// WBG (White Balance Gains) processor  
89/// ExifTool: %Image::ExifTool::MinoltaRaw::WBG hash (lines 113-137)
90struct MinoltaWbgProcessor {
91    /// Tag definitions for WBG block
92    tag_definitions: HashMap<u8, WbgTagDef>,
93}
94
95/// RIF (Requested Image Format) processor
96/// ExifTool: %Image::ExifTool::MinoltaRaw::RIF hash (lines 140-346)
97struct MinoltaRifProcessor {
98    /// Tag definitions for RIF block
99    tag_definitions: HashMap<u8, RifTagDef>,
100}
101
102/// PRD tag definition
103/// ExifTool: Individual entries in %Image::ExifTool::MinoltaRaw::PRD
104#[derive(Debug, Clone)]
105struct PrdTagDef {
106    /// Tag name
107    /// ExifTool: Name field in tag hash
108    name: String,
109
110    /// Data format
111    /// ExifTool: Format field
112    format: MinoltaFormat,
113
114    /// Optional raw conversion
115    /// ExifTool: RawConv field
116    raw_conv: Option<String>,
117}
118
119/// WBG tag definition
120/// ExifTool: Individual entries in %Image::ExifTool::MinoltaRaw::WBG
121#[derive(Debug, Clone)]
122struct WbgTagDef {
123    /// Tag name
124    /// ExifTool: Name field in tag hash
125    name: String,
126
127    /// Data format
128    /// ExifTool: Format field
129    format: MinoltaFormat,
130
131    /// Conditional processing
132    /// ExifTool: Condition field for DiMAGE A200 vs other models
133    condition: Option<WbgCondition>,
134}
135
136/// RIF tag definition
137/// ExifTool: Individual entries in %Image::ExifTool::MinoltaRaw::RIF
138#[derive(Debug, Clone)]
139struct RifTagDef {
140    /// Tag name
141    /// ExifTool: Name field in tag hash
142    name: String,
143
144    /// Data format
145    /// ExifTool: Format field (defaults to int8u if not specified)
146    format: MinoltaFormat,
147
148    /// Print conversion
149    /// ExifTool: PrintConv field
150    print_conv: Option<RifPrintConv>,
151
152    /// Value conversion
153    /// ExifTool: ValueConv field
154    value_conv: Option<RifValueConv>,
155
156    /// Raw conversion
157    /// ExifTool: RawConv field
158    raw_conv: Option<RifRawConv>,
159
160    /// Conditional processing
161    /// ExifTool: Condition field for Sony vs Minolta models
162    condition: Option<RifCondition>,
163}
164
165/// Minolta data formats
166/// ExifTool: Format specifications in tag definitions
167#[derive(Debug, Clone)]
168enum MinoltaFormat {
169    /// String with specified length
170    /// ExifTool: string[N] format
171    String(usize),
172
173    /// 8-bit unsigned integer
174    /// ExifTool: int8u format (default for RIF)
175    Int8u,
176
177    /// 8-bit signed integer
178    /// ExifTool: int8s format
179    Int8s,
180
181    /// 16-bit unsigned integer
182    /// ExifTool: int16u format
183    Int16u,
184
185    /// Array of 16-bit unsigned integers
186    /// ExifTool: int16u[N] format
187    Int16uArray(usize),
188
189    /// Array of 8-bit unsigned integers
190    /// ExifTool: int8u[N] format
191    Int8uArray(usize),
192}
193
194/// WBG conditional processing
195/// ExifTool: MinoltaRaw.pm lines 125-136 - DiMAGE A200 special case
196#[derive(Debug, Clone)]
197enum WbgCondition {
198    /// DiMAGE A200 model
199    /// ExifTool: line 126 - Condition => '$$self{Model} =~ /DiMAGE A200\b/'
200    DiMageA200,
201
202    /// Other models (default)
203    /// ExifTool: line 132 - other models case
204    Other,
205}
206
207/// RIF print conversion types
208/// ExifTool: PrintConv fields in RIF tags
209#[derive(Debug, Clone)]
210enum RifPrintConv {
211    /// White balance mode conversion
212    /// ExifTool: line 161 - PrintConv => 'Image::ExifTool::MinoltaRaw::ConvertWBMode($val)'
213    WbMode,
214
215    /// Program mode lookup
216    /// ExifTool: lines 165-175 - PrintConv hash
217    ProgramMode,
218
219    /// ISO setting conversion
220    /// ExifTool: lines 179-194 - complex ISO conversion with formula
221    IsoSetting,
222
223    /// Color mode conversion (Minolta)
224    /// ExifTool: line 207 - PrintConv => \%Image::ExifTool::Minolta::minoltaColorMode
225    ColorModeMinolta,
226
227    /// Color mode conversion (Sony A100)
228    /// ExifTool: line 216 - PrintConv => \%Image::ExifTool::Minolta::sonyColorMode
229    ColorModeSony,
230}
231
232/// RIF value conversion types
233/// ExifTool: ValueConv fields in RIF tags
234#[derive(Debug, Clone)]
235enum RifValueConv {
236    /// Color temperature scaling
237    /// ExifTool: line 297 - ValueConv => '$val * 100'
238    ColorTemperature,
239}
240
241/// RIF raw conversion types
242/// ExifTool: RawConv fields in RIF tags
243#[derive(Debug, Clone)]
244enum RifRawConv {
245    /// Exclude value 255
246    /// ExifTool: line 178 - RawConv => '$val == 255 ? undef : $val'
247    ExcludeU8Max,
248}
249
250/// RIF conditional processing
251/// ExifTool: Condition fields in RIF tags for Make/Model detection
252#[derive(Debug, Clone)]
253enum RifCondition {
254    /// Not Sony make
255    /// ExifTool: line 204 - Condition => '$$self{Make} !~ /^SONY/'
256    NotSony,
257
258    /// Sony A100 model
259    /// ExifTool: line 211 - Condition => '$$self{Model} eq "DSLR-A100"'
260    SonyA100,
261
262    /// Sony A100 or has PRD info
263    /// ExifTool: line 222 - Condition => '$$self{Model} eq "DSLR-A100" or $$self{MinoltaPRD}'
264    SonyA100OrPrd,
265
266    /// Sony A200/A700 models
267    /// ExifTool: line 327 - Condition => '$$self{Model} =~ /^DSLR-A(200|700)$/'
268    SonyA200A700,
269
270    /// Sony make
271    /// ExifTool: line 302 - Condition => '$$self{Make} =~ /^SONY/'
272    Sony,
273}
274
275impl Default for MinoltaRawHandler {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl MinoltaRawHandler {
282    /// Create new Minolta RAW handler with all processors
283    /// ExifTool: %Image::ExifTool::MinoltaRaw::Main hash construction
284    pub fn new() -> Self {
285        Self {
286            prd_processor: MinoltaPrdProcessor::new(),
287            wbg_processor: MinoltaWbgProcessor::new(),
288            rif_processor: MinoltaRifProcessor::new(),
289        }
290    }
291
292    /// Parse MRW file header and extract blocks
293    /// ExifTool: MinoltaRaw.pm ProcessMRW function lines 392-494
294    fn parse_mrw_header(&self, data: &[u8]) -> Result<MrwHeader> {
295        if data.len() < 8 {
296            return Err(ExifError::ParseError(
297                "MRW file too short for header".to_string(),
298            ));
299        }
300
301        // Parse magic bytes and determine byte order
302        // ExifTool: MinoltaRaw.pm lines 408-412
303        let byte_order = if data.starts_with(b"\0MRM") {
304            ByteOrder::BigEndian
305        } else if data.starts_with(b"\0MRI") {
306            ByteOrder::LittleEndian
307        } else {
308            return Err(ExifError::ParseError("Invalid MRW magic bytes".to_string()));
309        };
310
311        // Read data offset
312        // ExifTool: MinoltaRaw.pm line 420 - my $offset = Get32u(\$data, 4) + $pos
313        let data_offset = match byte_order {
314            ByteOrder::BigEndian => u32::from_be_bytes([data[4], data[5], data[6], data[7]]),
315            ByteOrder::LittleEndian => u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
316        };
317
318        let mut blocks = Vec::new();
319        let mut pos = 8;
320
321        // Loop through MRW segments
322        // ExifTool: MinoltaRaw.pm lines 424-476
323        while pos < data_offset as usize && pos + 8 <= data.len() {
324            // Read block tag (4 bytes)
325            // ExifTool: MinoltaRaw.pm line 427 - my $tag = substr($data, 0, 4)
326            let mut tag = [0u8; 4];
327            tag.copy_from_slice(&data[pos..pos + 4]);
328            pos += 4;
329
330            // Read block length
331            // ExifTool: MinoltaRaw.pm line 428 - my $len = Get32u(\$data, 4)
332            let length = match byte_order {
333                ByteOrder::BigEndian => {
334                    u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
335                }
336                ByteOrder::LittleEndian => {
337                    u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
338                }
339            };
340            pos += 4;
341
342            // Read block data
343            // ExifTool: MinoltaRaw.pm line 442 - $raf->Read($buff, $len)
344            if pos + length as usize > data.len() {
345                return Err(ExifError::ParseError(format!(
346                    "MRW block extends beyond file bounds: pos={}, length={}, file_size={}",
347                    pos,
348                    length,
349                    data.len()
350                )));
351            }
352
353            let block_data = data[pos..pos + length as usize].to_vec();
354            pos += length as usize;
355
356            blocks.push(MrwBlock {
357                tag,
358                length,
359                data: block_data,
360            });
361        }
362
363        Ok(MrwHeader {
364            byte_order,
365            data_offset,
366            blocks,
367        })
368    }
369}
370
371impl RawFormatHandler for MinoltaRawHandler {
372    /// Process Minolta MRW data
373    /// ExifTool: ProcessMRW function dispatches to block-specific processors
374    fn process_raw(&self, reader: &mut ExifReader, data: &[u8]) -> Result<()> {
375        // Parse MRW header and blocks
376        let header = self.parse_mrw_header(data)?;
377
378        // Process each block based on its type
379        // ExifTool: MinoltaRaw.pm lines 436-466 - block processing
380        for block in &header.blocks {
381            match &block.tag {
382                // TTW block - TIFF tags (processed by standard TIFF processor)
383                // ExifTool: lines 31-38 - TTW subdirectory with ProcessTIFF
384                b"\0TTW" => {
385                    // TODO: Process as TIFF subdirectory when TIFF processor is available
386                    // For now, skip TTW blocks as they require TIFF infrastructure
387                    continue;
388                }
389
390                // PRD block - Picture Raw Data
391                // ExifTool: lines 40-43 - PRD subdirectory
392                b"\0PRD" => {
393                    self.prd_processor
394                        .process(reader, &block.data, &header.byte_order)?;
395                }
396
397                // WBG block - White Balance Gains
398                // ExifTool: lines 44-47 - WBG subdirectory
399                b"\0WBG" => {
400                    self.wbg_processor
401                        .process(reader, &block.data, &header.byte_order)?;
402                }
403
404                // RIF block - Requested Image Format
405                // ExifTool: lines 48-51 - RIF subdirectory
406                b"\0RIF" => {
407                    self.rif_processor
408                        .process(reader, &block.data, &header.byte_order)?;
409                }
410
411                // CSA block is padding - skip
412                // ExifTool: line 52 - "# "\0CSA" is padding"
413                b"\0CSA" => {
414                    continue;
415                }
416
417                // Unknown block - skip with warning
418                _ => {
419                    let tag_str = String::from_utf8_lossy(&block.tag);
420                    eprintln!("Warning: Unknown MRW block type: {tag_str}");
421                    continue;
422                }
423            }
424        }
425
426        Ok(())
427    }
428
429    fn name(&self) -> &'static str {
430        "MinoltaRaw"
431    }
432
433    fn validate_format(&self, data: &[u8]) -> bool {
434        // ExifTool: MinoltaRaw.pm validation logic
435        super::super::detector::validate_minolta_mrw_magic(data)
436    }
437}
438
439impl MinoltaPrdProcessor {
440    /// Create new PRD processor with tag definitions
441    /// ExifTool: %Image::ExifTool::MinoltaRaw::PRD hash (lines 56-110)
442    fn new() -> Self {
443        let mut tag_definitions = HashMap::new();
444
445        // FirmwareID at offset 0
446        // ExifTool: line 64 - 0 => { Name => 'FirmwareID', Format => 'string[8]', ... }
447        tag_definitions.insert(
448            0,
449            PrdTagDef {
450                name: "FirmwareID".to_string(),
451                format: MinoltaFormat::String(8),
452                raw_conv: Some(
453                    "$$self{MinoltaPRD} = 1 if $$self{FILE_TYPE} eq \"MRW\"; $val".to_string(),
454                ),
455            },
456        );
457
458        // SensorHeight at offset 8
459        // ExifTool: line 69 - 8 => { Name => 'SensorHeight', Format => 'int16u' }
460        tag_definitions.insert(
461            8,
462            PrdTagDef {
463                name: "SensorHeight".to_string(),
464                format: MinoltaFormat::Int16u,
465                raw_conv: None,
466            },
467        );
468
469        // SensorWidth at offset 10
470        // ExifTool: line 73 - 10 => { Name => 'SensorWidth', Format => 'int16u' }
471        tag_definitions.insert(
472            10,
473            PrdTagDef {
474                name: "SensorWidth".to_string(),
475                format: MinoltaFormat::Int16u,
476                raw_conv: None,
477            },
478        );
479
480        // ImageHeight at offset 12
481        // ExifTool: line 77 - 12 => { Name => 'ImageHeight', Format => 'int16u' }
482        tag_definitions.insert(
483            12,
484            PrdTagDef {
485                name: "ImageHeight".to_string(),
486                format: MinoltaFormat::Int16u,
487                raw_conv: None,
488            },
489        );
490
491        // ImageWidth at offset 14
492        // ExifTool: line 81 - 14 => { Name => 'ImageWidth', Format => 'int16u' }
493        tag_definitions.insert(
494            14,
495            PrdTagDef {
496                name: "ImageWidth".to_string(),
497                format: MinoltaFormat::Int16u,
498                raw_conv: None,
499            },
500        );
501
502        // RawDepth at offset 16
503        // ExifTool: line 85 - 16 => { Name => 'RawDepth', Format => 'int8u' }
504        tag_definitions.insert(
505            16,
506            PrdTagDef {
507                name: "RawDepth".to_string(),
508                format: MinoltaFormat::Int8u,
509                raw_conv: None,
510            },
511        );
512
513        // BitDepth at offset 17
514        // ExifTool: line 89 - 17 => { Name => 'BitDepth', Format => 'int8u' }
515        tag_definitions.insert(
516            17,
517            PrdTagDef {
518                name: "BitDepth".to_string(),
519                format: MinoltaFormat::Int8u,
520                raw_conv: None,
521            },
522        );
523
524        // StorageMethod at offset 18
525        // ExifTool: line 93 - 18 => { Name => 'StorageMethod', Format => 'int8u', PrintConv => {...} }
526        tag_definitions.insert(
527            18,
528            PrdTagDef {
529                name: "StorageMethod".to_string(),
530                format: MinoltaFormat::Int8u,
531                raw_conv: None,
532            },
533        );
534
535        // BayerPattern at offset 23
536        // ExifTool: line 101 - 23 => { Name => 'BayerPattern', Format => 'int8u', PrintConv => {...} }
537        tag_definitions.insert(
538            23,
539            PrdTagDef {
540                name: "BayerPattern".to_string(),
541                format: MinoltaFormat::Int8u,
542                raw_conv: None,
543            },
544        );
545
546        Self { tag_definitions }
547    }
548
549    /// Process PRD block data
550    /// ExifTool: ProcessBinaryData with MinoltaRaw::PRD table
551    fn process(&self, reader: &mut ExifReader, data: &[u8], byte_order: &ByteOrder) -> Result<()> {
552        // Process each tag definition
553        for (&offset, tag_def) in &self.tag_definitions {
554            let tag_value =
555                self.extract_value(data, offset as usize, &tag_def.format, byte_order)?;
556
557            // Store the tag with source info
558            // ExifTool: GROUPS => { 0 => 'MakerNotes', 2 => 'Camera' }
559            let source_info = TagSourceInfo::new(
560                "MakerNotes".to_string(),
561                "MinoltaPRD".to_string(),
562                "Camera".to_string(),
563            );
564
565            // Use offset as tag ID for PRD tags
566            let tag_id = 0x1000 + offset as u16; // Offset to avoid conflicts
567            reader.extracted_tags.insert(tag_id, tag_value);
568            reader.tag_sources.insert(tag_id, source_info);
569        }
570
571        Ok(())
572    }
573
574    /// Extract value from binary data based on format
575    /// ExifTool: ProcessBinaryData value extraction
576    fn extract_value(
577        &self,
578        data: &[u8],
579        offset: usize,
580        format: &MinoltaFormat,
581        byte_order: &ByteOrder,
582    ) -> Result<TagValue> {
583        match format {
584            MinoltaFormat::String(length) => {
585                if offset + length > data.len() {
586                    return Err(ExifError::ParseError(format!(
587                        "String at offset {offset:#x} extends beyond data bounds"
588                    )));
589                }
590                let string_bytes = &data[offset..offset + length];
591                let string_value = String::from_utf8_lossy(string_bytes).to_string();
592                Ok(TagValue::String(string_value))
593            }
594            MinoltaFormat::Int8u => {
595                if offset >= data.len() {
596                    return Err(ExifError::ParseError(format!(
597                        "Int8u at offset {offset:#x} extends beyond data bounds"
598                    )));
599                }
600                Ok(TagValue::U8(data[offset]))
601            }
602            MinoltaFormat::Int16u => {
603                if offset + 2 > data.len() {
604                    return Err(ExifError::ParseError(format!(
605                        "Int16u at offset {offset:#x} extends beyond data bounds"
606                    )));
607                }
608                let value = match byte_order {
609                    ByteOrder::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
610                    ByteOrder::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
611                };
612                Ok(TagValue::U16(value))
613            }
614            _ => Err(ExifError::ParseError(format!(
615                "Unsupported format in PRD processor: {format:?}"
616            ))),
617        }
618    }
619}
620
621impl MinoltaWbgProcessor {
622    /// Create new WBG processor with tag definitions
623    /// ExifTool: %Image::ExifTool::MinoltaRaw::WBG hash (lines 113-137)
624    fn new() -> Self {
625        let mut tag_definitions = HashMap::new();
626
627        // WBScale at offset 0
628        // ExifTool: line 120 - 0 => { Name => 'WBScale', Format => 'int8u[4]' }
629        tag_definitions.insert(
630            0,
631            WbgTagDef {
632                name: "WBScale".to_string(),
633                format: MinoltaFormat::Int8uArray(4),
634                condition: None,
635            },
636        );
637
638        // WB levels at offset 4 - conditional on camera model
639        // ExifTool: lines 124-136 - conditional WB_GBRGLevels vs WB_RGGBLevels
640        tag_definitions.insert(
641            4,
642            WbgTagDef {
643                name: "WB_RGGBLevels".to_string(), // Default case
644                format: MinoltaFormat::Int16uArray(4),
645                condition: Some(WbgCondition::Other),
646            },
647        );
648
649        Self { tag_definitions }
650    }
651
652    /// Process WBG block data
653    /// ExifTool: ProcessBinaryData with MinoltaRaw::WBG table
654    fn process(&self, reader: &mut ExifReader, data: &[u8], byte_order: &ByteOrder) -> Result<()> {
655        // Process each tag definition
656        for (&offset, tag_def) in &self.tag_definitions {
657            let tag_value =
658                self.extract_value(data, offset as usize, &tag_def.format, byte_order)?;
659
660            // Store the tag with source info
661            // ExifTool: GROUPS => { 0 => 'MakerNotes', 2 => 'Camera' }
662            let source_info = TagSourceInfo::new(
663                "MakerNotes".to_string(),
664                "MinoltaWBG".to_string(),
665                "Camera".to_string(),
666            );
667
668            // Use offset as tag ID for WBG tags
669            let tag_id = 0x2000 + offset as u16; // Offset to avoid conflicts
670            reader.extracted_tags.insert(tag_id, tag_value);
671            reader.tag_sources.insert(tag_id, source_info);
672        }
673
674        Ok(())
675    }
676
677    /// Extract value from binary data based on format
678    /// ExifTool: ProcessBinaryData value extraction
679    fn extract_value(
680        &self,
681        data: &[u8],
682        offset: usize,
683        format: &MinoltaFormat,
684        byte_order: &ByteOrder,
685    ) -> Result<TagValue> {
686        match format {
687            MinoltaFormat::Int8uArray(count) => {
688                if offset + count > data.len() {
689                    return Err(ExifError::ParseError(format!(
690                        "Int8u array at offset {offset:#x} extends beyond data bounds"
691                    )));
692                }
693                let values = data[offset..offset + count].to_vec();
694                Ok(TagValue::U8Array(values))
695            }
696            MinoltaFormat::Int16uArray(count) => {
697                let total_bytes = count * 2;
698                if offset + total_bytes > data.len() {
699                    return Err(ExifError::ParseError(format!(
700                        "Int16u array at offset {offset:#x} extends beyond data bounds"
701                    )));
702                }
703
704                let mut values = Vec::new();
705                for i in 0..*count {
706                    let item_offset = offset + i * 2;
707                    let value = match byte_order {
708                        ByteOrder::BigEndian => {
709                            u16::from_be_bytes([data[item_offset], data[item_offset + 1]])
710                        }
711                        ByteOrder::LittleEndian => {
712                            u16::from_le_bytes([data[item_offset], data[item_offset + 1]])
713                        }
714                    };
715                    values.push(value);
716                }
717                Ok(TagValue::U16Array(values))
718            }
719            _ => Err(ExifError::ParseError(format!(
720                "Unsupported format in WBG processor: {format:?}"
721            ))),
722        }
723    }
724}
725
726impl MinoltaRifProcessor {
727    /// Create new RIF processor with tag definitions
728    /// ExifTool: %Image::ExifTool::MinoltaRaw::RIF hash (lines 140-346)
729    fn new() -> Self {
730        let mut tag_definitions = HashMap::new();
731
732        // Saturation at offset 1
733        // ExifTool: line 147 - 1 => { Name => 'Saturation', Format => 'int8s' }
734        tag_definitions.insert(
735            1,
736            RifTagDef {
737                name: "Saturation".to_string(),
738                format: MinoltaFormat::Int8s,
739                print_conv: None,
740                value_conv: None,
741                raw_conv: None,
742                condition: None,
743            },
744        );
745
746        // Contrast at offset 2
747        // ExifTool: line 151 - 2 => { Name => 'Contrast', Format => 'int8s' }
748        tag_definitions.insert(
749            2,
750            RifTagDef {
751                name: "Contrast".to_string(),
752                format: MinoltaFormat::Int8s,
753                print_conv: None,
754                value_conv: None,
755                raw_conv: None,
756                condition: None,
757            },
758        );
759
760        // Sharpness at offset 3
761        // ExifTool: line 155 - 3 => { Name => 'Sharpness', Format => 'int8s' }
762        tag_definitions.insert(
763            3,
764            RifTagDef {
765                name: "Sharpness".to_string(),
766                format: MinoltaFormat::Int8s,
767                print_conv: None,
768                value_conv: None,
769                raw_conv: None,
770                condition: None,
771            },
772        );
773
774        // WBMode at offset 4
775        // ExifTool: line 159 - 4 => { Name => 'WBMode', PrintConv => 'Image::ExifTool::MinoltaRaw::ConvertWBMode($val)' }
776        tag_definitions.insert(
777            4,
778            RifTagDef {
779                name: "WBMode".to_string(),
780                format: MinoltaFormat::Int8u, // Default format
781                print_conv: Some(RifPrintConv::WbMode),
782                value_conv: None,
783                raw_conv: None,
784                condition: None,
785            },
786        );
787
788        // ProgramMode at offset 5
789        // ExifTool: line 163 - 5 => { Name => 'ProgramMode', PrintConv => {...} }
790        tag_definitions.insert(
791            5,
792            RifTagDef {
793                name: "ProgramMode".to_string(),
794                format: MinoltaFormat::Int8u,
795                print_conv: Some(RifPrintConv::ProgramMode),
796                value_conv: None,
797                raw_conv: None,
798                condition: None,
799            },
800        );
801
802        // ISOSetting at offset 6
803        // ExifTool: line 176 - 6 => { Name => 'ISOSetting', RawConv => '$val == 255 ? undef : $val', PrintConv => {...} }
804        tag_definitions.insert(
805            6,
806            RifTagDef {
807                name: "ISOSetting".to_string(),
808                format: MinoltaFormat::Int8u,
809                print_conv: Some(RifPrintConv::IsoSetting),
810                value_conv: None,
811                raw_conv: Some(RifRawConv::ExcludeU8Max),
812                condition: None,
813            },
814        );
815
816        Self { tag_definitions }
817    }
818
819    /// Process RIF block data
820    /// ExifTool: ProcessBinaryData with MinoltaRaw::RIF table
821    fn process(&self, reader: &mut ExifReader, data: &[u8], byte_order: &ByteOrder) -> Result<()> {
822        // Process each tag definition
823        for (&offset, tag_def) in &self.tag_definitions {
824            let tag_value =
825                self.extract_value(data, offset as usize, &tag_def.format, byte_order)?;
826
827            // Store the tag with source info
828            // ExifTool: GROUPS => { 0 => 'MakerNotes', 2 => 'Image' }
829            let source_info = TagSourceInfo::new(
830                "MakerNotes".to_string(),
831                "MinoltaRIF".to_string(),
832                "Image".to_string(),
833            );
834
835            // Use offset as tag ID for RIF tags
836            let tag_id = 0x3000 + offset as u16; // Offset to avoid conflicts
837            reader.extracted_tags.insert(tag_id, tag_value);
838            reader.tag_sources.insert(tag_id, source_info);
839        }
840
841        Ok(())
842    }
843
844    /// Extract value from binary data based on format
845    /// ExifTool: ProcessBinaryData value extraction
846    fn extract_value(
847        &self,
848        data: &[u8],
849        offset: usize,
850        format: &MinoltaFormat,
851        byte_order: &ByteOrder,
852    ) -> Result<TagValue> {
853        match format {
854            MinoltaFormat::Int8u => {
855                if offset >= data.len() {
856                    return Err(ExifError::ParseError(format!(
857                        "Int8u at offset {offset:#x} extends beyond data bounds"
858                    )));
859                }
860                Ok(TagValue::U8(data[offset]))
861            }
862            MinoltaFormat::Int8s => {
863                if offset >= data.len() {
864                    return Err(ExifError::ParseError(format!(
865                        "Int8s at offset {offset:#x} extends beyond data bounds"
866                    )));
867                }
868                // Convert signed 8-bit to signed 16-bit since TagValue doesn't have I8
869                Ok(TagValue::I16(data[offset] as i8 as i16))
870            }
871            _ => Err(ExifError::ParseError(format!(
872                "Unsupported format in RIF processor: {format:?}"
873            ))),
874        }
875    }
876}
877
878/// Get Minolta MRW tag name by ID
879/// ExifTool: lib/Image/ExifTool/MinoltaRaw.pm tag definitions
880pub fn get_minolta_tag_name(tag_id: u16) -> Option<&'static str> {
881    match tag_id {
882        // PRD tags (0x1000-0x1FFF range)
883        0x1000 => Some("FirmwareID"),
884        0x1008 => Some("SensorHeight"),
885        0x100A => Some("SensorWidth"),
886        0x100C => Some("ImageHeight"),
887        0x100E => Some("ImageWidth"),
888        0x1010 => Some("RawDepth"),
889        0x1011 => Some("BitDepth"),
890        0x1012 => Some("StorageMethod"),
891        0x1017 => Some("BayerPattern"),
892
893        // WBG tags (0x2000-0x2FFF range)
894        0x2000 => Some("WBScale"),
895        0x2004 => Some("WB_RGGBLevels"),
896
897        // RIF tags (0x3000-0x3FFF range)
898        0x3001 => Some("Saturation"),
899        0x3002 => Some("Contrast"),
900        0x3003 => Some("Sharpness"),
901        0x3004 => Some("WBMode"),
902        0x3005 => Some("ProgramMode"),
903        0x3006 => Some("ISOSetting"),
904
905        _ => None,
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912
913    #[test]
914    fn test_minolta_handler_creation() {
915        let handler = MinoltaRawHandler::new();
916        assert_eq!(handler.name(), "MinoltaRaw");
917    }
918
919    #[test]
920    fn test_get_minolta_tag_name() {
921        // Test PRD tags
922        assert_eq!(get_minolta_tag_name(0x1000), Some("FirmwareID"));
923        assert_eq!(get_minolta_tag_name(0x1008), Some("SensorHeight"));
924        assert_eq!(get_minolta_tag_name(0x100C), Some("ImageHeight"));
925
926        // Test WBG tags
927        assert_eq!(get_minolta_tag_name(0x2000), Some("WBScale"));
928        assert_eq!(get_minolta_tag_name(0x2004), Some("WB_RGGBLevels"));
929
930        // Test RIF tags
931        assert_eq!(get_minolta_tag_name(0x3001), Some("Saturation"));
932        assert_eq!(get_minolta_tag_name(0x3004), Some("WBMode"));
933
934        // Test unknown tag
935        assert_eq!(get_minolta_tag_name(0x9999), None);
936    }
937
938    #[test]
939    fn test_mrw_header_parsing() {
940        let handler = MinoltaRawHandler::new();
941
942        // Test valid MRW big-endian header
943        let mrw_data = {
944            let mut data = vec![0u8; 100];
945            data[0..4].copy_from_slice(b"\0MRM");
946            data[4..8].copy_from_slice(&32u32.to_be_bytes()); // data offset
947                                                              // Add a test block
948            data[8..12].copy_from_slice(b"\0PRD");
949            data[12..16].copy_from_slice(&16u32.to_be_bytes()); // block length
950            data[16..32].fill(0x42); // block data
951            data
952        };
953
954        let header = handler.parse_mrw_header(&mrw_data).unwrap();
955        assert!(matches!(header.byte_order, ByteOrder::BigEndian));
956        assert_eq!(header.data_offset, 32);
957        assert_eq!(header.blocks.len(), 1);
958        assert_eq!(&header.blocks[0].tag, b"\0PRD");
959        assert_eq!(header.blocks[0].length, 16);
960    }
961
962    #[test]
963    fn test_format_validation() {
964        let handler = MinoltaRawHandler::new();
965
966        // Test valid MRW magic
967        let valid_data = b"\0MRM\x00\x00\x00\x20test_data";
968        assert!(handler.validate_format(valid_data));
969
970        // Test invalid magic
971        let invalid_data = b"\0MRX\x00\x00\x00\x20test_data";
972        assert!(!handler.validate_format(invalid_data));
973
974        // Test insufficient data
975        let short_data = b"\0MR";
976        assert!(!handler.validate_format(short_data));
977    }
978}