Skip to main content

exiftool_rs/metadata/
exif.rs

1//! EXIF/TIFF IFD metadata reader.
2//!
3//! Implements reading of TIFF IFD structures used in EXIF, GPS, and Interop metadata.
4//! Mirrors the core logic of ExifTool's Exif.pm ProcessExif function.
5
6use byteorder::{BigEndian, ByteOrder, LittleEndian};
7
8use crate::error::{Error, Result};
9use crate::tag::{Tag, TagGroup, TagId};
10use crate::tags::exif as exif_tags;
11use crate::value::Value;
12
13/// Byte order of the TIFF data.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ByteOrderMark {
16    LittleEndian,
17    BigEndian,
18}
19
20/// Parsed TIFF header.
21#[derive(Debug)]
22pub struct TiffHeader {
23    pub byte_order: ByteOrderMark,
24    pub ifd0_offset: u32,
25}
26
27/// EXIF IFD entry as read from the file.
28#[derive(Debug)]
29struct IfdEntry {
30    tag: u16,
31    data_type: u16,
32    count: u32,
33    value_offset: u32,
34    /// For values that fit in 4 bytes, the raw 4 bytes
35    inline_data: [u8; 4],
36}
37
38/// Size in bytes for each TIFF data type.
39fn type_size(data_type: u16) -> Option<usize> {
40    match data_type {
41        1 => Some(1),  // BYTE
42        2 => Some(1),  // ASCII
43        3 => Some(2),  // SHORT
44        4 => Some(4),  // LONG
45        5 => Some(8),  // RATIONAL
46        6 => Some(1),  // SBYTE
47        7 => Some(1),  // UNDEFINED
48        8 => Some(2),  // SSHORT
49        9 => Some(4),  // SLONG
50        10 => Some(8), // SRATIONAL
51        11 => Some(4), // FLOAT
52        12 => Some(8), // DOUBLE
53        13 => Some(4), // IFD
54        _ => None,
55    }
56}
57
58/// Parse a TIFF header from raw bytes.
59pub fn parse_tiff_header(data: &[u8]) -> Result<TiffHeader> {
60    if data.len() < 8 {
61        return Err(Error::InvalidTiffHeader);
62    }
63
64    let byte_order = match (data[0], data[1]) {
65        (b'I', b'I') => ByteOrderMark::LittleEndian,
66        (b'M', b'M') => ByteOrderMark::BigEndian,
67        _ => return Err(Error::InvalidTiffHeader),
68    };
69
70    let magic = match byte_order {
71        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[2..4]),
72        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[2..4]),
73    };
74
75    if magic != 42 {
76        return Err(Error::InvalidTiffHeader);
77    }
78
79    let ifd0_offset = match byte_order {
80        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[4..8]),
81        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[4..8]),
82    };
83
84    Ok(TiffHeader {
85        byte_order,
86        ifd0_offset,
87    })
88}
89
90/// EXIF metadata reader.
91pub struct ExifReader;
92
93impl ExifReader {
94    /// Parse EXIF data from a byte slice (starting at the TIFF header).
95    pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
96        let header = parse_tiff_header(data)?;
97        let mut tags = Vec::new();
98
99        // Emit ExifByteOrder tag
100        let bo_str = match header.byte_order {
101            ByteOrderMark::LittleEndian => "Little-endian (Intel, II)",
102            ByteOrderMark::BigEndian => "Big-endian (Motorola, MM)",
103        };
104        tags.push(Tag {
105            id: TagId::Text("ExifByteOrder".to_string()),
106            name: "ExifByteOrder".to_string(),
107            description: "Exif Byte Order".to_string(),
108            group: TagGroup {
109                family0: "EXIF".to_string(),
110                family1: "IFD0".to_string(),
111                family2: "ExifTool".to_string(),
112            },
113            raw_value: Value::String(bo_str.to_string()),
114            print_value: bo_str.to_string(),
115            priority: 0,
116        });
117
118        // Detect CR2: "CR" at offset 8 in TIFF data
119        let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
120
121        // Read IFD0 (main image)
122        Self::read_ifd(data, &header, header.ifd0_offset, "IFD0", &mut tags)?;
123
124        // For CR2 files, rename IFD0 StripOffsets→PreviewImageStart and
125        // StripByteCounts→PreviewImageLength, then construct PreviewImage.
126        if is_cr2 {
127            // Rename tags in-place
128            for tag in tags.iter_mut() {
129                if tag.group.family1 == "IFD0" {
130                    if tag.name == "StripOffsets" {
131                        tag.name = "PreviewImageStart".to_string();
132                        tag.description = "Preview Image Start".to_string();
133                        tag.id = TagId::Text("PreviewImageStart".to_string());
134                    } else if tag.name == "StripByteCounts" {
135                        tag.name = "PreviewImageLength".to_string();
136                        tag.description = "Preview Image Length".to_string();
137                        tag.id = TagId::Text("PreviewImageLength".to_string());
138                    }
139                }
140            }
141            // Construct PreviewImage from PreviewImageStart + PreviewImageLength
142            let preview_start = tags.iter()
143                .find(|t| t.name == "PreviewImageStart" && t.group.family1 == "IFD0")
144                .and_then(|t| t.raw_value.as_u64())
145                .map(|v| v as usize);
146            let preview_len = tags.iter()
147                .find(|t| t.name == "PreviewImageLength" && t.group.family1 == "IFD0")
148                .and_then(|t| t.raw_value.as_u64())
149                .map(|v| v as usize);
150            if let (Some(start), Some(len)) = (preview_start, preview_len) {
151                if len > 0 && start + len <= data.len() {
152                    let img_data = data[start..start+len].to_vec();
153                    let pv = format!("(Binary data {} bytes, use -b option to extract)", len);
154                    tags.push(Tag {
155                        id: TagId::Text("PreviewImage".to_string()),
156                        name: "PreviewImage".to_string(),
157                        description: "Preview Image".to_string(),
158                        group: TagGroup {
159                            family0: "EXIF".to_string(),
160                            family1: "IFD0".to_string(),
161                            family2: "Preview".to_string(),
162                        },
163                        raw_value: Value::Binary(img_data),
164                        print_value: pv,
165                        priority: 0,
166                    });
167                }
168            }
169        }
170
171        // Extract Make + Model for MakerNotes detection and sub-table dispatch
172        let make = tags
173            .iter()
174            .find(|t| t.name == "Make")
175            .map(|t| t.print_value.clone())
176            .unwrap_or_default();
177
178        let model = tags
179            .iter()
180            .find(|t| t.name == "Model")
181            .map(|t| t.print_value.clone())
182            .unwrap_or_default();
183
184        // Store model for sub-table dispatch
185        let make_and_model = if model.is_empty() { make.clone() } else { model };
186
187        // Find and parse MakerNotes
188        // Look for the MakerNote tag (0x927C) that was stored as Undefined
189        let mn_info: Option<(usize, usize)> = {
190            // Re-scan ExifIFD for MakerNote offset/size
191            let mut result = None;
192            Self::find_makernote(data, &header, &mut result);
193            result
194        };
195
196        if let Some((mn_offset, mn_size)) = mn_info {
197            let mn_tags = crate::metadata::makernotes::parse_makernotes(
198                data, mn_offset, mn_size, &make, &make_and_model, header.byte_order,
199            );
200            // Remove the raw MakerNote tag and replace with parsed tags
201            tags.retain(|t| t.name != "MakerNote");
202            // In Perl ExifTool, MakerNotes tags with equal/higher priority overwrite EXIF tags.
203            // Tags in the EXIF-primary list: EXIF wins (skip MakerNotes duplicate).
204            // Other tags: MakerNotes wins (remove EXIF version, add MakerNotes version).
205            {
206                // Tags where EXIF takes priority over MakerNotes (structural/authoritative EXIF)
207                let exif_primary: &[&str] = &[
208                    "ThumbnailOffset", "ThumbnailLength", "ThumbnailImage",
209                    "StripOffsets", "StripByteCounts",
210                    "PreviewImageStart", "PreviewImageLength", "PreviewImage",
211                    "ImageWidth", "ImageHeight", "BitsPerSample", "Compression",
212                    "PhotometricInterpretation", "SamplesPerPixel", "RowsPerStrip",
213                    "PlanarConfiguration", "XResolution", "YResolution", "ResolutionUnit",
214                    "Orientation", "Make", "Model", "Software",
215                    "ExifByteOrder", "CR2CFAPattern", "RawImageSegmentation",
216                    "ColorSpace", "ExifVersion", "FlashpixVersion",
217                    "ExifImageWidth", "ExifImageHeight", "InteropIndex", "InteropVersion",
218                    "DateTimeOriginal", "CreateDate", "ModifyDate", "DateTime",
219                    "FocalPlaneXResolution", "FocalPlaneYResolution", "FocalPlaneResolutionUnit",
220                    "CustomRendered", "ExposureMode", "SceneCaptureType",
221                    "Flash", "FocalLength", "ISO", "ExposureTime", "ExposureProgram",
222                    "FNumber", "ShutterSpeedValue", "ApertureValue", "ComponentsConfiguration",
223                    "UserComment",
224                ];
225                let mn_name_set: std::collections::HashSet<String> = mn_tags.iter()
226                    .map(|t| t.name.clone())
227                    .collect();
228                let exif_has: std::collections::HashSet<String> = tags.iter()
229                    .map(|t| t.name.clone())
230                    .collect();
231                // Remove EXIF non-primary tags when MakerNotes provides them (MakerNotes wins)
232                tags.retain(|t| {
233                    !mn_name_set.contains(&t.name)
234                    || exif_primary.contains(&t.name.as_str())
235                });
236                // Add MakerNotes tags, but skip EXIF-primary tags that EXIF already provides
237                for mn_tag in mn_tags {
238                    if exif_primary.contains(&mn_tag.name.as_str())
239                        && exif_has.contains(&mn_tag.name) {
240                        // EXIF wins - don't add MakerNotes version
241                        continue;
242                    }
243                    tags.push(mn_tag);
244                }
245            }
246        }
247
248        // DNG PrivateData (0xC634): parse Adobe MakN for MakerNotes if no MakerNote found
249        if mn_info.is_none() {
250            // Scan for DNGPrivateData in tags — look for "Adobe\0" header
251            // Find the offset from the tag value (stored as binary/undefined)
252            Self::parse_dng_private_data(data, &header, &make, &make_and_model, &mut tags);
253        }
254
255        // Parse IPTC data embedded in TIFF (tag 0x83BB "IPTC-NAA")
256        // The raw tag stores IPTC data as undefined bytes or a list of u32 values
257        {
258            let iptc_data: Option<Vec<u8>> = tags.iter()
259                .find(|t| t.name == "IPTC-NAA")
260                .and_then(|t| {
261                    match &t.raw_value {
262                        Value::Undefined(bytes) => Some(bytes.clone()),
263                        Value::Binary(bytes) => Some(bytes.clone()),
264                        Value::List(items) => {
265                            // IPTC-NAA stored as uint32 list - convert back to bytes (big-endian)
266                            let mut bytes = Vec::with_capacity(items.len() * 4);
267                            for item in items {
268                                match item {
269                                    Value::U32(v) => bytes.extend_from_slice(&v.to_be_bytes()),
270                                    _ => {}
271                                }
272                            }
273                            if bytes.is_empty() { None } else { Some(bytes) }
274                        }
275                        _ => None,
276                    }
277                });
278
279            if let Some(iptc_bytes) = iptc_data {
280                // Compute MD5 of the raw IPTC data for CurrentIPTCDigest
281                let md5_hex = crate::md5::md5_hex(&iptc_bytes);
282
283                if let Ok(iptc_tags) = crate::metadata::IptcReader::read(&iptc_bytes) {
284                    // Replace raw IPTC-NAA tag with parsed IPTC tags
285                    tags.retain(|t| t.name != "IPTC-NAA");
286                    tags.extend(iptc_tags);
287                }
288
289                // Add CurrentIPTCDigest tag
290                tags.push(crate::tag::Tag {
291                    id: crate::tag::TagId::Text("CurrentIPTCDigest".into()),
292                    name: "CurrentIPTCDigest".into(),
293                    description: "Current IPTC Digest".into(),
294                    group: crate::tag::TagGroup {
295                        family0: "IPTC".into(),
296                        family1: "IPTC".into(),
297                        family2: "Other".into(),
298                    },
299                    raw_value: Value::String(md5_hex.clone()),
300                    print_value: md5_hex,
301                    priority: 0,
302                });
303            }
304        }
305
306        // Parse ICC_Profile data embedded in TIFF (tag 0x8773)
307        {
308            let icc_data: Option<Vec<u8>> = tags.iter()
309                .find(|t| t.name == "ICC_Profile")
310                .and_then(|t| {
311                    match &t.raw_value {
312                        Value::Undefined(bytes) => Some(bytes.clone()),
313                        Value::Binary(bytes) => Some(bytes.clone()),
314                        _ => None,
315                    }
316                });
317
318            if let Some(icc_bytes) = icc_data {
319                if let Ok(icc_tags) = crate::formats::icc::read_icc(&icc_bytes) {
320                    // Replace raw ICC_Profile tag with parsed ICC tags
321                    tags.retain(|t| t.name != "ICC_Profile");
322                    tags.extend(icc_tags);
323                }
324            }
325        }
326
327        // Process GeoTIFF key directory if present
328        process_geotiff_keys(&mut tags);
329
330        // Final deduplication: within MakerNotes, if the same tag name appears multiple times
331        // (e.g., from different sub-tables), keep the last occurrence.
332        // Only deduplicate MakerNotes tags (family0 == "MakerNotes") to avoid affecting
333        // structural EXIF/IFD tags.
334        {
335            // Find MakerNotes tags that have duplicates
336            let mn_tags_start = tags.iter().position(|t| t.group.family0 == "MakerNotes")
337                .unwrap_or(tags.len());
338            if mn_tags_start < tags.len() {
339                // For each MakerNotes tag name, find the last occurrence index
340                let mut last_idx: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
341                for (i, t) in tags[mn_tags_start..].iter().enumerate() {
342                    last_idx.insert(t.name.as_str(), i + mn_tags_start);
343                }
344                // Retain only the last occurrence of each MakerNotes duplicate
345                let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
346                // Process in reverse, keeping only the last (= rightmost = first in reverse)
347                let mut keep = vec![false; tags.len()];
348                for (i, t) in tags.iter().enumerate().rev() {
349                    if t.group.family0 != "MakerNotes" {
350                        keep[i] = true;
351                        continue;
352                    }
353                    if seen.insert(t.name.as_str()) {
354                        keep[i] = true; // first seen in reverse = last occurrence
355                    }
356                }
357                let mut iter = keep.iter();
358                tags.retain(|_| *iter.next().unwrap_or(&true));
359            }
360        }
361
362        Ok(tags)
363    }
364
365    /// Find MakerNote (tag 0x927C) offset and size in ExifIFD.
366    /// Parse DNG PrivateData (0xC634) to extract embedded MakerNotes
367    fn parse_dng_private_data(data: &[u8], header: &TiffHeader, make: &str, model: &str, tags: &mut Vec<Tag>) {
368        // Scan IFD0 for tag 0xC634
369        let ifd0_offset = header.ifd0_offset as usize;
370        if ifd0_offset + 2 > data.len() { return; }
371        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
372        let entries_start = ifd0_offset + 2;
373        for i in 0..entry_count {
374            let eoff = entries_start + i * 12;
375            if eoff + 12 > data.len() { break; }
376            let tag = read_u16(data, eoff, header.byte_order);
377            if tag == 0xC634 {
378                let dtype = read_u16(data, eoff + 2, header.byte_order);
379                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
380                let elem_size = match dtype { 1 | 7 => 1, _ => 0 };
381                let total = elem_size * count;
382                if total < 14 { continue; }
383                let off = read_u32(data, eoff + 8, header.byte_order) as usize;
384                if off + total > data.len() { continue; }
385                let pdata = &data[off..off+total];
386                // Parse Adobe DNGPrivateData: "Adobe\0" + blocks
387                if !pdata.starts_with(b"Adobe\0") { continue; }
388                let mut bpos = 6;
389                while bpos + 8 <= pdata.len() {
390                    let btag = &pdata[bpos..bpos+4];
391                    let bsize = u32::from_be_bytes([pdata[bpos+4], pdata[bpos+5], pdata[bpos+6], pdata[bpos+7]]) as usize;
392                    bpos += 8;
393                    if bpos + bsize > pdata.len() { break; }
394                    if btag == b"MakN" && bsize > 6 {
395                        let mn_block = &pdata[bpos..bpos+bsize];
396                        let mn_bo = if &mn_block[0..2] == b"II" {
397                            ByteOrderMark::LittleEndian
398                        } else {
399                            ByteOrderMark::BigEndian
400                        };
401                        let mut mn_start = 6; // skip byte order + original offset
402                        // Hack for extra 12 bytes in MakN header (Adobe Camera Raw bug)
403                        if bsize >= 18 && &mn_block[6..10] == b"\0\0\0\x01" {
404                            mn_start += 12;
405                        }
406                        if mn_start < bsize {
407                            // Emit MakerNoteByteOrder
408                            let mn_bo_str = if mn_bo == ByteOrderMark::LittleEndian {
409                                "Little-endian (Intel, II)"
410                            } else {
411                                "Big-endian (Motorola, MM)"
412                            };
413                            tags.push(Tag {
414                                id: TagId::Text("MakerNoteByteOrder".into()),
415                                name: "MakerNoteByteOrder".into(),
416                                description: "Maker Note Byte Order".into(),
417                                group: TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
418                                raw_value: Value::String(mn_bo_str.into()),
419                                print_value: mn_bo_str.into(),
420                                priority: 0,
421                            });
422                            // Canon MakerNotes have a TIFF footer with the original offset.
423                            // Sub-table value_offsets are relative to the original file.
424                            // We need to pass the full DNG data so offsets resolve correctly.
425                            let mn_data_in_block = &mn_block[mn_start..];
426                            let mn_abs_offset = off + (bpos - 8 + 8) + mn_start; // absolute offset in DNG file
427                            // Check for Canon TIFF footer (last 8 bytes)
428                            let fix_base = if mn_data_in_block.len() > 8 {
429                                let footer = &mn_data_in_block[mn_data_in_block.len()-8..];
430                                if (footer[0..2] == *b"II" || footer[0..2] == *b"MM")
431                                    && (footer[2..4] == *b"\x2a\x00" || footer[2..4] == *b"\x00\x2a")
432                                {
433                                    let old_off = if footer[0] == b'I' {
434                                        u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]])
435                                    } else {
436                                        u32::from_be_bytes([footer[4], footer[5], footer[6], footer[7]])
437                                    } as usize;
438                                    if old_off > 0 && mn_abs_offset > old_off {
439                                        mn_abs_offset as isize - old_off as isize
440                                    } else { 0 }
441                                } else { 0 }
442                            } else { 0 };
443
444                            let mn_tags = if fix_base != 0 {
445                                // Pass full DNG data with corrected offset and base fix
446                                crate::metadata::makernotes::parse_makernotes_with_base(
447                                    data, mn_abs_offset, mn_data_in_block.len(),
448                                    make, model, mn_bo, fix_base,
449                                )
450                            } else {
451                                crate::metadata::makernotes::parse_makernotes(
452                                    mn_data_in_block, 0, mn_data_in_block.len(),
453                                    make, model, mn_bo,
454                                )
455                            };
456                            // DNG MakerNote tags that Perl doesn't emit (conditions/Unknown/offset issues)
457                            let dng_suppress = ["AESetting", "CameraISO", "ImageStabilization",
458                                "SpotMeteringMode", "RawJpgSize", "Warning"];
459                            for mn_tag in mn_tags {
460                                // Skip EXIF-primary tags and DNG-suppressed tags
461                                if tags.iter().any(|t| t.name == mn_tag.name) { continue; }
462                                if dng_suppress.contains(&mn_tag.name.as_str()) { continue; }
463                                tags.push(mn_tag);
464                            }
465                        }
466                    }
467                    bpos += bsize;
468                }
469                break;
470            }
471        }
472    }
473
474    fn find_makernote(data: &[u8], header: &TiffHeader, result: &mut Option<(usize, usize)>) {
475        // First find ExifIFD offset from IFD0
476        let ifd0_offset = header.ifd0_offset as usize;
477        if ifd0_offset + 2 > data.len() {
478            return;
479        }
480        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
481        let entries_start = ifd0_offset + 2;
482
483        for i in 0..entry_count {
484            let eoff = entries_start + i * 12;
485            if eoff + 12 > data.len() { break; }
486            let tag = read_u16(data, eoff, header.byte_order);
487            if tag == 0x8769 {
488                // ExifIFD pointer
489                let exif_offset = read_u32(data, eoff + 8, header.byte_order) as usize;
490                Self::find_makernote_in_ifd(data, header, exif_offset, result);
491                break;
492            }
493        }
494    }
495
496    fn find_makernote_in_ifd(data: &[u8], header: &TiffHeader, ifd_offset: usize, result: &mut Option<(usize, usize)>) {
497        if ifd_offset + 2 > data.len() {
498            return;
499        }
500        let entry_count = read_u16(data, ifd_offset, header.byte_order) as usize;
501        let entries_start = ifd_offset + 2;
502
503        for i in 0..entry_count {
504            let eoff = entries_start + i * 12;
505            if eoff + 12 > data.len() { break; }
506            let tag = read_u16(data, eoff, header.byte_order);
507            if tag == 0x927C {
508                let data_type = read_u16(data, eoff + 2, header.byte_order);
509                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
510                let type_size = match data_type { 1 | 2 | 6 | 7 => 1, 3 | 8 => 2, 4 | 9 | 11 | 13 => 4, 5 | 10 | 12 => 8, _ => 1 };
511                let total_size = type_size * count;
512
513                if total_size <= 4 {
514                    // Inline - too small for real MakerNotes
515                    break;
516                }
517                let offset = read_u32(data, eoff + 8, header.byte_order) as usize;
518                if offset + total_size <= data.len() {
519                    *result = Some((offset, total_size));
520                }
521                break;
522            }
523        }
524    }
525
526    /// Parse EXIF data from a byte slice with an explicit byte order and offset.
527    fn read_ifd(
528        data: &[u8],
529        header: &TiffHeader,
530        offset: u32,
531        ifd_name: &str,
532        tags: &mut Vec<Tag>,
533    ) -> Result<Option<u32>> {
534        let offset = offset as usize;
535        if offset + 2 > data.len() {
536            return Err(Error::InvalidExif(format!(
537                "{} offset {} beyond data length {}",
538                ifd_name,
539                offset,
540                data.len()
541            )));
542        }
543
544        let entry_count = read_u16(data, offset, header.byte_order) as usize;
545        let entries_start = offset + 2;
546        let _entries_end = entries_start + entry_count * 12;
547
548        // Validate: at minimum, first entry must fit
549        if entries_start + 12 > data.len() && entry_count > 0 {
550            return Err(Error::InvalidExif(format!(
551                "{} entries extend beyond data (need {}, have {})",
552                ifd_name,
553                entries_start + 12,
554                data.len()
555            )));
556        }
557        // Clamp entry count if IFD extends beyond data
558        let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
559        let entries_end = entries_start + entry_count * 12;
560
561        for i in 0..entry_count {
562            let entry_offset = entries_start + i * 12;
563            let entry = parse_ifd_entry(data, entry_offset, header.byte_order);
564
565            // Check for sub-IFDs (ExifIFD, GPS, Interop)
566            match entry.tag {
567                0x8769 => {
568                    // ExifIFD
569                    let sub_offset = entry.value_offset;
570                    if (sub_offset as usize) < data.len() {
571                        let _ = Self::read_ifd(data, header, sub_offset, "ExifIFD", tags);
572                    }
573                    continue;
574                }
575                0x8825 => {
576                    // GPS IFD
577                    let sub_offset = entry.value_offset;
578                    if (sub_offset as usize) < data.len() {
579                        let _ = Self::read_ifd(data, header, sub_offset, "GPS", tags);
580                    }
581                    continue;
582                }
583                0xA005 => {
584                    // Interop IFD
585                    let sub_offset = entry.value_offset;
586                    if (sub_offset as usize) < data.len() {
587                        let _ = Self::read_ifd(data, header, sub_offset, "InteropIFD", tags);
588                    }
589                    continue;
590                }
591                // PrintIM tag: extract version from "PrintIM" + 4-byte version
592                0xC4A5 => {
593                    let total_size = match entry.data_type {
594                        1 | 2 | 6 | 7 => entry.count as usize,
595                        _ => 0,
596                    };
597                    if total_size > 11 {
598                        let off = entry.value_offset as usize;
599                        if off + 11 <= data.len() && &data[off..off+7] == b"PrintIM" {
600                            let ver = String::from_utf8_lossy(&data[off+7..off+11]).to_string();
601                            tags.push(Tag {
602                                id: TagId::Text("PrintIMVersion".into()),
603                                name: "PrintIMVersion".into(),
604                                description: "PrintIM Version".into(),
605                                group: TagGroup { family0: "PrintIM".into(), family1: "PrintIM".into(), family2: "Printing".into() },
606                                raw_value: Value::String(ver.clone()),
607                                print_value: ver,
608                                priority: 0,
609                            });
610                        }
611                    }
612                    continue; // Suppress raw PrintIM tag
613                }
614                // Suppress GPS tag 0x0006 (GPSAltitude) when value is 0/0
615                0x0006 if ifd_name == "GPS" => {
616                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
617                        if let Value::URational(0, 0) = val {
618                            continue;
619                        }
620                    }
621                }
622                // In SubIFD, tag 0x0201 = JpgFromRawStart (JPEG preview offset)
623                0x0201 if ifd_name.starts_with("SubIFD") => {
624                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
625                        let pv = val.to_display_string();
626                        tags.push(Tag {
627                            id: TagId::Numeric(entry.tag),
628                            name: "JpgFromRawStart".into(),
629                            description: "Jpg From Raw Start".into(),
630                            group: TagGroup {
631                                family0: "EXIF".into(),
632                                family1: ifd_name.to_string(),
633                                family2: "Image".into(),
634                            },
635                            raw_value: val,
636                            print_value: pv,
637                            priority: 0,
638                        });
639                    }
640                    continue;
641                }
642                // In SubIFD, tag 0x0202 = JpgFromRawLength (JPEG preview byte count)
643                0x0202 if ifd_name.starts_with("SubIFD") => {
644                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
645                        let pv = val.to_display_string();
646                        tags.push(Tag {
647                            id: TagId::Numeric(entry.tag),
648                            name: "JpgFromRawLength".into(),
649                            description: "Jpg From Raw Length".into(),
650                            group: TagGroup {
651                                family0: "EXIF".into(),
652                                family1: ifd_name.to_string(),
653                                family2: "Image".into(),
654                            },
655                            raw_value: val,
656                            print_value: pv,
657                            priority: 0,
658                        });
659                    }
660                    continue;
661                }
662                // SubIFD pointer (0x014A): follow to read SubIFD entries
663                0x014A if ifd_name == "IFD0" => {
664                    // Read SubIFD offset(s) — may be a single uint32 or array
665                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
666                        let offsets: Vec<u32> = match &val {
667                            Value::U32(v) => vec![*v],
668                            Value::List(items) => items.iter().filter_map(|v| {
669                                if let Value::U32(o) = v { Some(*o) } else { None }
670                            }).collect(),
671                            _ => vec![],
672                        };
673                        for (idx, &off) in offsets.iter().enumerate() {
674                            if (off as usize) < data.len() {
675                                let sub_name = format!("SubIFD{}", idx);
676                                let before_idx = tags.len();
677                                let _ = Self::read_ifd(data, header, off, &sub_name, tags);
678
679                                // Check if this SubIFD has JPEG compression
680                                let is_jpeg = tags[before_idx..].iter()
681                                    .any(|t| t.name == "Compression" && (t.print_value.contains("JPEG") || t.raw_value.as_u64() == Some(6)));
682
683                                if is_jpeg {
684                                    // Rename StripOffsets/StripByteCounts based on SubIFD index
685                                    // Perl: SubIFD2 → JpgFromRaw*, others → PreviewImage*
686                                    let (start_name, len_name, img_name) = if idx == 2 {
687                                        ("JpgFromRawStart", "JpgFromRawLength", "JpgFromRaw")
688                                    } else {
689                                        ("PreviewImageStart", "PreviewImageLength", "PreviewImage")
690                                    };
691                                    // Find StripOffsets and StripByteCounts in this SubIFD
692                                    let strip_off = tags[before_idx..].iter()
693                                        .find(|t| t.name == "StripOffsets")
694                                        .and_then(|t| t.raw_value.as_u64());
695                                    let strip_len = tags[before_idx..].iter()
696                                        .find(|t| t.name == "StripByteCounts")
697                                        .and_then(|t| t.raw_value.as_u64());
698                                    if let (Some(s), Some(l)) = (strip_off, strip_len) {
699                                        tags.push(Tag {
700                                            id: TagId::Text(start_name.into()), name: start_name.into(),
701                                            description: start_name.into(),
702                                            group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
703                                            raw_value: Value::U32(s as u32), print_value: s.to_string(), priority: 0,
704                                        });
705                                        tags.push(Tag {
706                                            id: TagId::Text(len_name.into()), name: len_name.into(),
707                                            description: len_name.into(),
708                                            group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
709                                            raw_value: Value::U32(l as u32), print_value: l.to_string(), priority: 0,
710                                        });
711                                        // Extract binary image data
712                                        let s = s as usize;
713                                        let l = l as usize;
714                                        if l > 0 && s + l <= data.len() {
715                                            let pv = format!("(Binary data {} bytes, use -b option to extract)", l);
716                                            tags.push(Tag {
717                                                id: TagId::Text(img_name.into()), name: img_name.into(),
718                                                description: img_name.into(),
719                                                group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
720                                                raw_value: Value::Binary(data[s..s+l].to_vec()),
721                                                print_value: pv, priority: 0,
722                                            });
723                                        }
724                                    }
725                                }
726
727                                // Legacy: Also check for already-named JpgFromRawStart tags
728                                let jpg_start = tags[before_idx..].iter()
729                                    .find(|t| t.name == "JpgFromRawStart")
730                                    .and_then(|t| t.raw_value.as_u64());
731                                let jpg_len = tags[before_idx..].iter()
732                                    .find(|t| t.name == "JpgFromRawLength")
733                                    .and_then(|t| t.raw_value.as_u64());
734                                if let (Some(start), Some(len)) = (jpg_start, jpg_len) {
735                                    let start = start as usize;
736                                    let len = len as usize;
737                                    if len > 0 && start + len <= data.len() {
738                                        let pv = format!(
739                                            "(Binary data {} bytes, use -b option to extract)",
740                                            len
741                                        );
742                                        tags.push(Tag {
743                                            id: TagId::Text("JpgFromRaw".into()),
744                                            name: "JpgFromRaw".into(),
745                                            description: "Jpg From Raw".into(),
746                                            group: TagGroup {
747                                                family0: "EXIF".into(),
748                                                family1: sub_name,
749                                                family2: "Preview".into(),
750                                            },
751                                            raw_value: Value::Binary(
752                                                data[start..start + len].to_vec(),
753                                            ),
754                                            print_value: pv,
755                                            priority: 0,
756                                        });
757                                    }
758                                }
759                            }
760                        }
761                    }
762                    continue;
763                }
764                // In CR2 IFD2 (preview JPEG), suppress StripOffsets/StripByteCounts
765                // because IFD3 has the correct values for the raw data.
766                // Also suppress tags that duplicate IFD0 content (ImageWidth, ImageHeight,
767                // BitsPerSample, Compression) since the first (IFD0) value is preferred.
768                0x0100 | 0x0101 | 0x0102 | 0x0103 | 0x0111 | 0x0117
769                    if ifd_name == "IFD2" => {
770                    continue;
771                }
772                // In CR2 IFD3 (raw data), suppress Compression (IFD0 value is preferred).
773                0x0103 if ifd_name == "IFD3" => {
774                    continue;
775                }
776                _ => {}
777            }
778
779            if let Some(mut value) = read_ifd_value(data, &entry, header.byte_order) {
780                // GPS TimeStamp (0x0007): convert 0/0 rationals to 0/1 so it displays as "0, 0, 0"
781                // (Perl treats 0/0 as 0 for GPS time, enabling GPSDateTime composite)
782                if ifd_name == "GPS" && entry.tag == 0x0007 {
783                    if let Value::List(ref mut items) = value {
784                        for item in items.iter_mut() {
785                            if matches!(item, Value::URational(0, 0)) {
786                                *item = Value::URational(0, 1);
787                            }
788                        }
789                    }
790                }
791                let tag_info = exif_tags::lookup(ifd_name, entry.tag);
792                let (name, description, family2) = match tag_info {
793                    Some(info) => (
794                        info.name.to_string(),
795                        info.description.to_string(),
796                        info.family2.to_string(),
797                    ),
798                    None => {
799                        // Skip known SubDirectory/internal tags that Perl doesn't emit
800                        if matches!(entry.tag,
801                            // 0x014A handled above (SubIFD traversal)
802                            // 0x02BC (ApplicationNotes) now parsed as XMP above
803                            0xC634   // DNG PrivateData — processed after IFD scan
804                        ) {
805                            continue;
806                        }
807                        // Fallback to generated tags
808                        match exif_tags::lookup_generated(entry.tag) {
809                            Some((n, d)) => (n.to_string(), d.to_string(), "Other".to_string()),
810                            None => {
811                                // Perl doesn't emit unknown EXIF tags by default
812                                continue;
813                            },
814                        }
815                    }
816                };
817
818                // Parse ApplicationNotes (0x02BC) as XMP
819                if name == "ApplicationNotes" {
820                    if let Value::Binary(ref xmp_bytes) = value {
821                        if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_bytes) {
822                            tags.extend(xmp_tags);
823                        }
824                    }
825                    continue;
826                }
827                // Suppress known SubDirectory/internal tags
828                if matches!(name.as_str(),
829                    "MinSampleValue" | "MaxSampleValue" | // Not emitted by Perl for raw formats
830                    "ProcessingSoftware" | // Protected tag, not always emitted
831                    "PanasonicTitle" | "PanasonicTitle2" // DNG tags, wrong match for RW2
832                ) {
833                    continue;
834                }
835
836                let print_value =
837                    exif_tags::print_conv(ifd_name, entry.tag, &value)
838                        .or_else(|| {
839                            // Fallback to generated print conversions
840                            value.as_u64()
841                                .and_then(|v| crate::tags::print_conv_generated::print_conv_by_name(&name, v as i64))
842                                .map(|s| s.to_string())
843                        })
844                        .unwrap_or_else(|| value.to_display_string());
845
846                tags.push(Tag {
847                    id: TagId::Numeric(entry.tag),
848                    name,
849                    description,
850                    group: TagGroup {
851                        family0: "EXIF".to_string(),
852                        family1: ifd_name.to_string(),
853                        family2,
854                    },
855                    raw_value: value,
856                    print_value,
857                    priority: 0,
858                });
859            }
860        }
861
862        // Read next IFD offset
863        let next_ifd_offset = if entries_end + 4 <= data.len() {
864            read_u32(data, entries_end, header.byte_order)
865        } else { 0 };
866        if next_ifd_offset != 0 && ifd_name == "IFD0" {
867            // IFD1 = thumbnail
868            let ifd1_start_idx = tags.len();
869            let ifd1_next = Self::read_ifd(data, header, next_ifd_offset, "IFD1", tags)
870                .ok().flatten();
871            // Suppress IFD1 tags that duplicate IFD0 tags (only keep thumbnail-specific ones)
872            // In Perl, IFD1 (thumbnail) tags are secondary and don't appear in output if IFD0 has them.
873            {
874                let ifd0_names: std::collections::HashSet<String> = tags[..ifd1_start_idx].iter()
875                    .map(|t| t.name.clone())
876                    .collect();
877                let thumbnail_tags = ["ThumbnailOffset", "ThumbnailLength", "ThumbnailImage",
878                    "Compression", "PhotometricInterpretation", "JPEGInterchangeFormat",
879                    "JPEGInterchangeFormatLength",
880                    "SubfileType", "StripOffsets", "StripByteCounts"];
881                tags.retain(|t| {
882                    if t.group.family1 != "IFD1" { return true; }
883                    // Keep thumbnail-specific tags
884                    if thumbnail_tags.contains(&t.name.as_str()) { return true; }
885                    // Suppress IFD1 tags that IFD0 already has
886                    !ifd0_names.contains(&t.name)
887                });
888            }
889
890            // Create ThumbnailImage tag if offset+length are present
891            let thumb_offset = tags.iter()
892                .find(|t| t.name == "ThumbnailOffset" && t.group.family1 == "IFD1")
893                .and_then(|t| t.raw_value.as_u64());
894            let thumb_length = tags.iter()
895                .find(|t| t.name == "ThumbnailLength" && t.group.family1 == "IFD1")
896                .and_then(|t| t.raw_value.as_u64());
897
898            if let (Some(off), Some(len)) = (thumb_offset, thumb_length) {
899                let off = off as usize;
900                let len = len as usize;
901                if off + len <= data.len() && len > 0 {
902                    tags.push(Tag {
903                        id: TagId::Text("ThumbnailImage".into()),
904                        name: "ThumbnailImage".into(),
905                        description: "Thumbnail Image".into(),
906                        group: TagGroup { family0: "EXIF".into(), family1: "IFD1".into(), family2: "Image".into() },
907                        raw_value: Value::Binary(data[off..off+len].to_vec()),
908                        print_value: format!("(Binary data {} bytes)", len),
909                        priority: 0,
910                    });
911                }
912            }
913
914            // CR2 files have additional IFDs (IFD2, IFD3) following IFD1 in the chain.
915            // CR2 is identified by "CR" bytes at offset 8 in the TIFF data.
916            let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
917            if is_cr2 {
918                if let Some(ifd2_offset) = ifd1_next {
919                    // IFD2 = preview JPEG image data (emit selected tags)
920                    let ifd2_next = Self::read_ifd(data, header, ifd2_offset, "IFD2", tags)
921                        .ok().flatten();
922                    // IFD3 = raw image data (emit CR2CFAPattern, RawImageSegmentation, StripOffsets, StripByteCounts)
923                    if let Some(ifd3_offset) = ifd2_next {
924                        let _ = Self::read_ifd(data, header, ifd3_offset, "IFD3", tags);
925                    }
926                }
927            }
928        }
929
930        Ok(if next_ifd_offset != 0 {
931            Some(next_ifd_offset)
932        } else {
933            None
934        })
935    }
936
937    /// Parse a TIFF where IFD0 is treated as a named IFD (e.g. "GPS", "ExifIFD").
938    /// Used for CR3 CMT4 (GPS-only TIFF) and CMT2 (ExifIFD-only TIFF).
939    /// Does NOT emit ExifByteOrder or do MakerNote/IFD1 processing.
940    pub fn read_as_named_ifd(data: &[u8], ifd_name: &str) -> Vec<Tag> {
941        let header = match parse_tiff_header(data) {
942            Ok(h) => h,
943            Err(_) => return Vec::new(),
944        };
945        let mut tags = Vec::new();
946        let _ = Self::read_ifd(data, &header, header.ifd0_offset, ifd_name, &mut tags);
947        tags
948    }
949}
950
951fn parse_ifd_entry(data: &[u8], offset: usize, byte_order: ByteOrderMark) -> IfdEntry {
952    let tag = read_u16(data, offset, byte_order);
953    let data_type = read_u16(data, offset + 2, byte_order);
954    let count = read_u32(data, offset + 4, byte_order);
955    let value_offset = read_u32(data, offset + 8, byte_order);
956    let mut inline_data = [0u8; 4];
957    inline_data.copy_from_slice(&data[offset + 8..offset + 12]);
958
959    IfdEntry {
960        tag,
961        data_type,
962        count,
963        value_offset,
964        inline_data,
965    }
966}
967
968fn read_ifd_value(data: &[u8], entry: &IfdEntry, byte_order: ByteOrderMark) -> Option<Value> {
969    let elem_size = type_size(entry.data_type)?;
970    let total_size = elem_size * entry.count as usize;
971
972    let value_data = if total_size <= 4 {
973        &entry.inline_data[..total_size]
974    } else {
975        let offset = entry.value_offset as usize;
976        if offset + total_size > data.len() {
977            return None;
978        }
979        &data[offset..offset + total_size]
980    };
981
982    // IPTC-NAA (0x83BB): always read as raw binary regardless of declared type
983    if entry.tag == 0x83BB {
984        return Some(Value::Binary(value_data.to_vec()));
985    }
986
987    // ApplicationNotes (0x02BC): always read as raw binary (XMP data)
988    if entry.tag == 0x02BC {
989        return Some(Value::Binary(value_data.to_vec()));
990    }
991
992    match entry.data_type {
993        // BYTE
994        1 => {
995            if entry.count == 1 {
996                Some(Value::U8(value_data[0]))
997            } else {
998                Some(Value::List(value_data.iter().map(|&b| Value::U8(b)).collect()))
999            }
1000        }
1001        // ASCII
1002        2 => {
1003            let s = String::from_utf8_lossy(value_data);
1004            Some(Value::String(s.trim_end_matches('\0').to_string()))
1005        }
1006        // SHORT
1007        3 => {
1008            if entry.count == 1 {
1009                Some(Value::U16(read_u16(value_data, 0, byte_order)))
1010            } else {
1011                let vals: Vec<Value> = (0..entry.count as usize)
1012                    .map(|i| Value::U16(read_u16(value_data, i * 2, byte_order)))
1013                    .collect();
1014                Some(Value::List(vals))
1015            }
1016        }
1017        // LONG
1018        4 | 13 => {
1019            if entry.count == 1 {
1020                Some(Value::U32(read_u32(value_data, 0, byte_order)))
1021            } else {
1022                let vals: Vec<Value> = (0..entry.count as usize)
1023                    .map(|i| Value::U32(read_u32(value_data, i * 4, byte_order)))
1024                    .collect();
1025                Some(Value::List(vals))
1026            }
1027        }
1028        // RATIONAL (unsigned)
1029        5 => {
1030            if entry.count == 1 {
1031                let n = read_u32(value_data, 0, byte_order);
1032                let d = read_u32(value_data, 4, byte_order);
1033                Some(Value::URational(n, d))
1034            } else {
1035                let vals: Vec<Value> = (0..entry.count as usize)
1036                    .map(|i| {
1037                        let n = read_u32(value_data, i * 8, byte_order);
1038                        let d = read_u32(value_data, i * 8 + 4, byte_order);
1039                        Value::URational(n, d)
1040                    })
1041                    .collect();
1042                Some(Value::List(vals))
1043            }
1044        }
1045        // SBYTE
1046        6 => {
1047            if entry.count == 1 {
1048                Some(Value::I16(value_data[0] as i8 as i16))
1049            } else {
1050                let vals: Vec<Value> = value_data
1051                    .iter()
1052                    .map(|&b| Value::I16(b as i8 as i16))
1053                    .collect();
1054                Some(Value::List(vals))
1055            }
1056        }
1057        // UNDEFINED
1058        7 => Some(Value::Undefined(value_data.to_vec())),
1059        // SSHORT
1060        8 => {
1061            if entry.count == 1 {
1062                Some(Value::I16(read_i16(value_data, 0, byte_order)))
1063            } else {
1064                let vals: Vec<Value> = (0..entry.count as usize)
1065                    .map(|i| Value::I16(read_i16(value_data, i * 2, byte_order)))
1066                    .collect();
1067                Some(Value::List(vals))
1068            }
1069        }
1070        // SLONG
1071        9 => {
1072            if entry.count == 1 {
1073                Some(Value::I32(read_i32(value_data, 0, byte_order)))
1074            } else {
1075                let vals: Vec<Value> = (0..entry.count as usize)
1076                    .map(|i| Value::I32(read_i32(value_data, i * 4, byte_order)))
1077                    .collect();
1078                Some(Value::List(vals))
1079            }
1080        }
1081        // SRATIONAL
1082        10 => {
1083            if entry.count == 1 {
1084                let n = read_i32(value_data, 0, byte_order);
1085                let d = read_i32(value_data, 4, byte_order);
1086                Some(Value::IRational(n, d))
1087            } else {
1088                let vals: Vec<Value> = (0..entry.count as usize)
1089                    .map(|i| {
1090                        let n = read_i32(value_data, i * 8, byte_order);
1091                        let d = read_i32(value_data, i * 8 + 4, byte_order);
1092                        Value::IRational(n, d)
1093                    })
1094                    .collect();
1095                Some(Value::List(vals))
1096            }
1097        }
1098        // FLOAT
1099        11 => {
1100            if entry.count == 1 {
1101                let bits = read_u32(value_data, 0, byte_order);
1102                Some(Value::F32(f32::from_bits(bits)))
1103            } else {
1104                let vals: Vec<Value> = (0..entry.count as usize)
1105                    .map(|i| {
1106                        let bits = read_u32(value_data, i * 4, byte_order);
1107                        Value::F32(f32::from_bits(bits))
1108                    })
1109                    .collect();
1110                Some(Value::List(vals))
1111            }
1112        }
1113        // DOUBLE
1114        12 => {
1115            if entry.count == 1 {
1116                let bits = read_u64(value_data, 0, byte_order);
1117                Some(Value::F64(f64::from_bits(bits)))
1118            } else {
1119                let vals: Vec<Value> = (0..entry.count as usize)
1120                    .map(|i| {
1121                        let bits = read_u64(value_data, i * 8, byte_order);
1122                        Value::F64(f64::from_bits(bits))
1123                    })
1124                    .collect();
1125                Some(Value::List(vals))
1126            }
1127        }
1128        _ => None,
1129    }
1130}
1131
1132// Byte-order-aware read helpers
1133fn read_u16(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1134    match bo {
1135        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[offset..]),
1136        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[offset..]),
1137    }
1138}
1139
1140fn read_u32(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1141    match bo {
1142        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[offset..]),
1143        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[offset..]),
1144    }
1145}
1146
1147fn read_u64(data: &[u8], offset: usize, bo: ByteOrderMark) -> u64 {
1148    match bo {
1149        ByteOrderMark::LittleEndian => LittleEndian::read_u64(&data[offset..]),
1150        ByteOrderMark::BigEndian => BigEndian::read_u64(&data[offset..]),
1151    }
1152}
1153
1154fn read_i16(data: &[u8], offset: usize, bo: ByteOrderMark) -> i16 {
1155    match bo {
1156        ByteOrderMark::LittleEndian => LittleEndian::read_i16(&data[offset..]),
1157        ByteOrderMark::BigEndian => BigEndian::read_i16(&data[offset..]),
1158    }
1159}
1160
1161fn read_i32(data: &[u8], offset: usize, bo: ByteOrderMark) -> i32 {
1162    match bo {
1163        ByteOrderMark::LittleEndian => LittleEndian::read_i32(&data[offset..]),
1164        ByteOrderMark::BigEndian => BigEndian::read_i32(&data[offset..]),
1165    }
1166}
1167
1168/// Process GeoTIFF key directory (tag GeoTiffDirectory / GeoKeyDirectory)
1169/// and replace raw directory/ascii/double params with named GeoTIFF tags.
1170fn process_geotiff_keys(tags: &mut Vec<Tag>) {
1171    // Extract GeoTiffDirectory values
1172    let dir_vals: Option<Vec<u16>> = tags.iter()
1173        .find(|t| t.name == "GeoTiffDirectory")
1174        .and_then(|t| {
1175            match &t.raw_value {
1176                Value::List(items) => {
1177                    let vals: Vec<u16> = items.iter().filter_map(|v| {
1178                        match v {
1179                            Value::U16(x) => Some(*x),
1180                            Value::U32(x) => Some(*x as u16),
1181                            _ => None,
1182                        }
1183                    }).collect();
1184                    if vals.is_empty() { None } else { Some(vals) }
1185                }
1186                _ => None,
1187            }
1188        });
1189
1190    let dir_vals = match dir_vals {
1191        Some(v) => v,
1192        None => return,
1193    };
1194
1195    if dir_vals.len() < 4 {
1196        return;
1197    }
1198
1199    let version = dir_vals[0];
1200    let revision = dir_vals[1];
1201    let minor_rev = dir_vals[2];
1202    let num_entries = dir_vals[3] as usize;
1203
1204    if dir_vals.len() < 4 + num_entries * 4 {
1205        return;
1206    }
1207
1208    // Extract ASCII params
1209    let ascii_params: Option<String> = tags.iter()
1210        .find(|t| t.name == "GeoTiffAsciiParams")
1211        .map(|t| t.print_value.clone());
1212
1213    // Extract double params
1214    let double_params: Option<Vec<f64>> = tags.iter()
1215        .find(|t| t.name == "GeoTiffDoubleParams")
1216        .and_then(|t| {
1217            match &t.raw_value {
1218                Value::List(items) => {
1219                    let vals: Vec<f64> = items.iter().filter_map(|v| {
1220                        match v {
1221                            Value::F64(x) => Some(*x),
1222                            Value::F32(x) => Some(*x as f64),
1223                            _ => None,
1224                        }
1225                    }).collect();
1226                    if vals.is_empty() { None } else { Some(vals) }
1227                }
1228                _ => None,
1229            }
1230        });
1231
1232    let mut new_tags = Vec::new();
1233
1234    // Version tag
1235    new_tags.push(Tag {
1236        id: TagId::Text("GeoTiffVersion".to_string()),
1237        name: "GeoTiffVersion".to_string(),
1238        description: "GeoTiff Version".to_string(),
1239        group: TagGroup { family0: "EXIF".into(), family1: "IFD0".into(), family2: "Location".into() },
1240        raw_value: Value::String(format!("{}.{}.{}", version, revision, minor_rev)),
1241        print_value: format!("{}.{}.{}", version, revision, minor_rev),
1242        priority: 0,
1243    });
1244
1245    // Process each GeoKey
1246    for i in 0..num_entries {
1247        let base = 4 + i * 4;
1248        let key_id = dir_vals[base];
1249        let location = dir_vals[base + 1];
1250        let count = dir_vals[base + 2] as usize;
1251        let value_or_offset = dir_vals[base + 3];
1252
1253        let raw_val: Option<String> = match location {
1254            0 => {
1255                // Value stored inline in value_or_offset
1256                Some(format!("{}", value_or_offset))
1257            }
1258            34737 => {
1259                // ASCII params
1260                if let Some(ref ascii) = ascii_params {
1261                    let off = value_or_offset as usize;
1262                    let end = (off + count).min(ascii.len());
1263                    if off <= end {
1264                        let s = &ascii[off..end];
1265                        // Remove trailing '|' separators
1266                        let s = s.trim_end_matches('|').trim().to_string();
1267                        Some(s)
1268                    } else {
1269                        None
1270                    }
1271                } else {
1272                    None
1273                }
1274            }
1275            34736 => {
1276                // Double params
1277                if let Some(ref doubles) = double_params {
1278                    let off = value_or_offset as usize;
1279                    if count == 1 && off < doubles.len() {
1280                        Some(format!("{}", doubles[off]))
1281                    } else if count > 1 {
1282                        let vals: Vec<String> = doubles.iter().skip(off).take(count)
1283                            .map(|v| format!("{}", v)).collect();
1284                        Some(vals.join(" "))
1285                    } else {
1286                        None
1287                    }
1288                } else {
1289                    None
1290                }
1291            }
1292            _ => None,
1293        };
1294
1295        let val_str = match raw_val {
1296            Some(v) => v,
1297            None => continue,
1298        };
1299
1300        // Map GeoKey ID to tag name and print value
1301        let (tag_name, print_val) = geotiff_key_to_tag(key_id, &val_str);
1302        if tag_name.is_empty() { continue; }
1303
1304        new_tags.push(Tag {
1305            id: TagId::Text(tag_name.clone()),
1306            name: tag_name.clone(),
1307            description: tag_name.clone(),
1308            group: TagGroup { family0: "EXIF".into(), family1: "IFD0".into(), family2: "Location".into() },
1309            raw_value: Value::String(val_str),
1310            print_value: print_val,
1311            priority: 0,
1312        });
1313    }
1314
1315    if !new_tags.is_empty() {
1316        // Remove raw GeoTIFF tags
1317        tags.retain(|t| t.name != "GeoTiffDirectory" && t.name != "GeoTiffAsciiParams" && t.name != "GeoTiffDoubleParams");
1318        tags.extend(new_tags);
1319    }
1320}
1321
1322/// Map a GeoKey ID to (tag_name, print_value).
1323fn geotiff_key_to_tag(key_id: u16, value: &str) -> (String, String) {
1324    let val_u16: Option<u16> = value.parse().ok();
1325
1326    match key_id {
1327        // Section 6.2.1: GeoTIFF Configuration Keys
1328        0x0001 => return ("GeoTiffVersion".to_string(), value.to_string()), // not used here
1329        0x0400 => { // GTModelType
1330            let print = match val_u16 {
1331                Some(1) => "Projected".to_string(),
1332                Some(2) => "Geographic".to_string(),
1333                Some(3) => "Geocentric".to_string(),
1334                Some(32767) => "User Defined".to_string(),
1335                _ => value.to_string(),
1336            };
1337            return ("GTModelType".to_string(), print);
1338        }
1339        0x0401 => { // GTRasterType
1340            let print = match val_u16 {
1341                Some(1) => "Pixel Is Area".to_string(),
1342                Some(2) => "Pixel Is Point".to_string(),
1343                Some(32767) => "User Defined".to_string(),
1344                _ => value.to_string(),
1345            };
1346            return ("GTRasterType".to_string(), print);
1347        }
1348        0x0402 => return ("GTCitation".to_string(), value.to_string()),
1349
1350        // Section 6.2.2: Geographic CS Parameter Keys
1351        0x0800 => return ("GeographicType".to_string(), geotiff_pcs_name(val_u16.unwrap_or(0), value)),
1352        0x0801 => return ("GeogCitation".to_string(), value.to_string()),
1353        0x0802 => return ("GeogGeodeticDatum".to_string(), value.to_string()),
1354        0x0803 => return ("GeogPrimeMeridian".to_string(), value.to_string()),
1355        0x0804 => return ("GeogLinearUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1356        0x0805 => return ("GeogLinearUnitSize".to_string(), value.to_string()),
1357        0x0806 => return ("GeogAngularUnits".to_string(), value.to_string()),
1358        0x0807 => return ("GeogAngularUnitSize".to_string(), value.to_string()),
1359        0x0808 => return ("GeogEllipsoid".to_string(), value.to_string()),
1360        0x0809 => return ("GeogSemiMajorAxis".to_string(), value.to_string()),
1361        0x080a => return ("GeogSemiMinorAxis".to_string(), value.to_string()),
1362        0x080b => return ("GeogInvFlattening".to_string(), value.to_string()),
1363        0x080c => return ("GeogAzimuthUnits".to_string(), value.to_string()),
1364        0x080d => return ("GeogPrimeMeridianLong".to_string(), value.to_string()),
1365
1366        // Section 6.2.3: Projected CS Parameter Keys
1367        0x0C00 => { // ProjectedCSType
1368            return ("ProjectedCSType".to_string(), geotiff_pcs_name(val_u16.unwrap_or(0), value));
1369        }
1370        0x0C01 => return ("PCSCitation".to_string(), value.to_string()),
1371        0x0C02 => return ("Projection".to_string(), value.to_string()),
1372        0x0C03 => return ("ProjCoordTrans".to_string(), value.to_string()),
1373        0x0C04 => return ("ProjLinearUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1374        0x0C05 => return ("ProjLinearUnitSize".to_string(), value.to_string()),
1375        0x0C06 => return ("ProjStdParallel1".to_string(), value.to_string()),
1376        0x0C07 => return ("ProjStdParallel2".to_string(), value.to_string()),
1377        0x0C08 => return ("ProjNatOriginLong".to_string(), value.to_string()),
1378        0x0C09 => return ("ProjNatOriginLat".to_string(), value.to_string()),
1379        0x0C0a => return ("ProjFalseEasting".to_string(), value.to_string()),
1380        0x0C0b => return ("ProjFalseNorthing".to_string(), value.to_string()),
1381        0x0C0c => return ("ProjFalseOriginLong".to_string(), value.to_string()),
1382        0x0C0d => return ("ProjFalseOriginLat".to_string(), value.to_string()),
1383        0x0C0e => return ("ProjFalseOriginEasting".to_string(), value.to_string()),
1384        0x0C0f => return ("ProjFalseOriginNorthing".to_string(), value.to_string()),
1385        0x0C10 => return ("ProjCenterLong".to_string(), value.to_string()),
1386        0x0C11 => return ("ProjCenterLat".to_string(), value.to_string()),
1387        0x0C12 => return ("ProjCenterEasting".to_string(), value.to_string()),
1388        0x0C13 => return ("ProjCenterNorthing".to_string(), value.to_string()),
1389        0x0C14 => return ("ProjScaleAtNatOrigin".to_string(), value.to_string()),
1390        0x0C15 => return ("ProjScaleAtCenter".to_string(), value.to_string()),
1391        0x0C16 => return ("ProjAzimuthAngle".to_string(), value.to_string()),
1392        0x0C17 => return ("ProjStraightVertPoleLong".to_string(), value.to_string()),
1393
1394        // Section 6.2.4: Vertical CS Keys
1395        0x1000 => return ("VerticalCSType".to_string(), value.to_string()),
1396        0x1001 => return ("VerticalCitation".to_string(), value.to_string()),
1397        0x1002 => return ("VerticalDatum".to_string(), value.to_string()),
1398        0x1003 => return ("VerticalUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1399
1400        _ => {}
1401    }
1402    (String::new(), String::new())
1403}
1404
1405fn geotiff_linear_unit_name(val: u16, fallback: &str) -> String {
1406    match val {
1407        9001 => "Linear Meter".to_string(),
1408        9002 => "Linear Foot".to_string(),
1409        9003 => "Linear Foot US Survey".to_string(),
1410        9004 => "Linear Foot Modified American".to_string(),
1411        9005 => "Linear Foot Clarke".to_string(),
1412        9006 => "Linear Foot Indian".to_string(),
1413        9007 => "Linear Link".to_string(),
1414        9008 => "Linear Link Benoit".to_string(),
1415        9009 => "Linear Link Sears".to_string(),
1416        9010 => "Linear Chain Benoit".to_string(),
1417        9011 => "Linear Chain Sears".to_string(),
1418        9012 => "Linear Yard Sears".to_string(),
1419        9013 => "Linear Yard Indian".to_string(),
1420        9014 => "Linear Fathom".to_string(),
1421        9015 => "Linear Mile International Nautical".to_string(),
1422        _ => fallback.to_string(),
1423    }
1424}
1425
1426fn geotiff_pcs_name(val: u16, fallback: &str) -> String {
1427    // Common PCS codes - just return the code with description for common ones
1428    match val {
1429        26918 => "NAD83 UTM zone 18N".to_string(),
1430        26919 => "NAD83 UTM zone 19N".to_string(),
1431        32618 => "WGS84 UTM zone 18N".to_string(),
1432        32619 => "WGS84 UTM zone 19N".to_string(),
1433        4326 => "WGS 84".to_string(),
1434        4269 => "NAD83".to_string(),
1435        4267 => "NAD27".to_string(),
1436        32767 => "User Defined".to_string(),
1437        _ => fallback.to_string(),
1438    }
1439}