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}