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 std::cell::{Cell, RefCell};
9
10use crate::error::{Error, Result};
11use crate::tag::{Tag, TagGroup, TagId};
12use crate::tags::exif as exif_tags;
13use crate::value::Value;
14
15thread_local! {
16    static SHOW_UNKNOWN: Cell<u8> = const { Cell::new(0) };
17    /// Whether every instance of a tag name must be reported. ExifTool's CLI turns
18    /// the Duplicates option on together with -ee, and `FoundTag` then keeps each
19    /// instance under its own key; the name-level pruning in this module reproduces
20    /// the Duplicates-off collapse and must be skipped in that case.
21    static KEEP_DUPLICATES: Cell<bool> = const { Cell::new(false) };
22    /// ExifTool's `$$self{TIFF_TYPE}`: which flavour of TIFF is being read.
23    /// Several tag names depend on it -- 0x0201 in IFD0 is a thumbnail offset
24    /// in a JPEG and a preview offset in an ARW -- and it is set once per file
25    /// rather than threaded through every IFD reader.
26    static TIFF_TYPE: RefCell<String> = const { RefCell::new(String::new()) };
27    /// ExifTool's `$$self{Software}`: the EXIF Software tag, stored as it is
28    /// read because MakerNote conditions ask for it. Sony tells one ILCE-9
29    /// firmware from another that way, and reads a different offset for each.
30    static SOFTWARE: RefCell<String> = const { RefCell::new(String::new()) };
31    /// ExifTool's `$$self{Make}`, kept for the same reason as Software: a
32    /// binary sub-table can be conditioned on it, and Kodak's Type9 reads four
33    /// of its fields only when the file says Kodak wrote it.
34    static MAKE: RefCell<String> = const { RefCell::new(String::new()) };
35}
36
37/// Set the Software string the file declares (ExifTool's `$$self{Software}`).
38pub fn set_software(text: &str) {
39    SOFTWARE.with(|s| s.borrow_mut().replace_range(.., text));
40}
41
42/// The Software string the file declares.
43#[must_use]
44pub fn software() -> String {
45    SOFTWARE.with(|s| s.borrow().clone())
46}
47
48/// Set the Make the file declares (ExifTool's `$$self{Make}`).
49pub fn set_make(text: &str) {
50    MAKE.with(|s| s.borrow_mut().replace_range(.., text));
51}
52
53/// The Make the file declares.
54#[must_use]
55pub fn make() -> String {
56    MAKE.with(|s| s.borrow().clone())
57}
58
59/// Set the TIFF flavour being read (ExifTool's `TIFF_TYPE`).
60pub fn set_tiff_type(kind: &str) {
61    TIFF_TYPE.with(|s| s.borrow_mut().replace_range(.., kind));
62}
63
64/// The TIFF flavour being read.
65#[must_use]
66pub fn tiff_type() -> String {
67    TIFF_TYPE.with(|s| s.borrow().clone())
68}
69
70/// Set whether duplicate tag names must all be kept (ExifTool's Duplicates option).
71pub fn set_keep_duplicates(keep: bool) {
72    KEEP_DUPLICATES.with(|s| s.set(keep));
73}
74
75/// Whether duplicate tag names must all be kept.
76pub fn keep_duplicates() -> bool {
77    KEEP_DUPLICATES.with(|s| s.get())
78}
79
80/// Set the show_unknown level for the current thread (used by MakerNotes).
81pub fn set_show_unknown(level: u8) {
82    SHOW_UNKNOWN.with(|s| s.set(level));
83}
84
85/// Get the show_unknown level for the current thread (used by MakerNotes).
86pub fn get_show_unknown() -> u8 {
87    SHOW_UNKNOWN.with(|s| s.get())
88}
89
90/// Byte order of the TIFF data.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ByteOrderMark {
93    LittleEndian,
94    BigEndian,
95}
96
97/// Parsed TIFF header.
98#[derive(Debug)]
99pub struct TiffHeader {
100    pub byte_order: ByteOrderMark,
101    pub ifd0_offset: u32,
102}
103
104/// EXIF IFD entry as read from the file.
105#[derive(Debug)]
106struct IfdEntry {
107    tag: u16,
108    data_type: u16,
109    count: u32,
110    value_offset: u32,
111    /// For values that fit in 4 bytes, the raw 4 bytes
112    inline_data: [u8; 4],
113}
114
115/// Size in bytes for each TIFF data type.
116fn type_size(data_type: u16) -> Option<usize> {
117    match data_type {
118        1 => Some(1),  // BYTE
119        2 => Some(1),  // ASCII
120        3 => Some(2),  // SHORT
121        4 => Some(4),  // LONG
122        5 => Some(8),  // RATIONAL
123        6 => Some(1),  // SBYTE
124        7 => Some(1),  // UNDEFINED
125        8 => Some(2),  // SSHORT
126        9 => Some(4),  // SLONG
127        10 => Some(8), // SRATIONAL
128        11 => Some(4), // FLOAT
129        12 => Some(8), // DOUBLE
130        13 => Some(4), // IFD
131        _ => None,
132    }
133}
134
135/// Parse a TIFF header from raw bytes.
136pub fn parse_tiff_header(data: &[u8]) -> Result<TiffHeader> {
137    if data.len() < 8 {
138        return Err(Error::InvalidTiffHeader);
139    }
140
141    let byte_order = match (data[0], data[1]) {
142        (b'I', b'I') => ByteOrderMark::LittleEndian,
143        (b'M', b'M') => ByteOrderMark::BigEndian,
144        _ => return Err(Error::InvalidTiffHeader),
145    };
146
147    let magic = match byte_order {
148        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[2..4]),
149        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[2..4]),
150    };
151
152    if magic != 42 {
153        return Err(Error::InvalidTiffHeader);
154    }
155
156    let ifd0_offset = match byte_order {
157        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[4..8]),
158        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[4..8]),
159    };
160
161    Ok(TiffHeader {
162        byte_order,
163        ifd0_offset,
164    })
165}
166
167/// Tags where the EXIF IFD value takes priority over a same-named MakerNotes tag
168/// (structural/authoritative EXIF). For all other duplicates, MakerNotes wins —
169/// matching ExifTool's group priority.
170pub(crate) const EXIF_PRIMARY_TAGS: &[&str] = &[
171    "ThumbnailOffset",
172    "ThumbnailLength",
173    "ThumbnailImage",
174    "StripOffsets",
175    "StripByteCounts",
176    "PreviewImageStart",
177    "PreviewImageLength",
178    "PreviewImage",
179    "ImageWidth",
180    "ImageHeight",
181    "BitsPerSample",
182    "Compression",
183    "PhotometricInterpretation",
184    "SamplesPerPixel",
185    "RowsPerStrip",
186    "PlanarConfiguration",
187    "XResolution",
188    "YResolution",
189    "ResolutionUnit",
190    "Orientation",
191    "Make",
192    "Model",
193    "Software",
194    "ExifByteOrder",
195    "CR2CFAPattern",
196    "RawImageSegmentation",
197    "ColorSpace",
198    "ExifVersion",
199    "FlashpixVersion",
200    "ExifImageWidth",
201    "ExifImageHeight",
202    "InteropIndex",
203    "InteropVersion",
204    "DateTimeOriginal",
205    "CreateDate",
206    "ModifyDate",
207    "DateTime",
208    "FocalPlaneXResolution",
209    "FocalPlaneYResolution",
210    "FocalPlaneResolutionUnit",
211    "CustomRendered",
212    "ExposureMode",
213    "SceneCaptureType",
214    // IFD0 PrintIM wins over a MakerNotes PrintIM copy.
215    "PrintIMVersion",
216    "Flash",
217    "FocalLength",
218    "ExposureTime",
219    // ExposureProgram is deliberately absent: Sony::Tag9404c states no priority
220    // of its own and is read after the ExifIFD, so ExifTool's own rule -- an
221    // incoming tag takes the name when its priority is >= the stored one's --
222    // gives it the name. Listing it here dropped the maker-note copy before the
223    // arbitration that implements that rule could see it.
224    "FNumber",
225    "ShutterSpeedValue",
226    "ApertureValue",
227    "ComponentsConfiguration",
228    "UserComment",
229    // Standard EXIF image-parameter tags (0xa408-0xa40c) take priority over the
230    // manufacturer's MakerNote duplicate when both are present (ExifTool default).
231    "Contrast",
232    "Saturation",
233    "Sharpness",
234];
235
236/// EXIF metadata reader.
237pub struct ExifReader;
238
239impl ExifReader {
240    /// Parse EXIF data from a byte slice (starting at the TIFF header).
241    pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
242        Self::read_with_base(data, 0)
243    }
244
245    /// Parse EXIF data, adding `base` (the TIFF header's offset within the file) to
246    /// offset-type tags so they read as absolute file offsets, matching ExifTool.
247    pub fn read_with_base(data: &[u8], base: usize) -> Result<Vec<Tag>> {
248        let mut tags = Self::read_inner(data, base)?;
249        if base != 0 {
250            // ExifTool reports these IsOffset tags relative to the start of the file.
251            const OFFSET_TAGS: &[&str] = &[
252                "ThumbnailOffset",
253                "PreviewImageStart",
254                "JpgFromRawStart",
255                "OtherImageStart",
256                "StripOffsets",
257            ];
258            for t in tags.iter_mut() {
259                if OFFSET_TAGS.contains(&t.name.as_str()) {
260                    if let Some(off) = t.raw_value.as_u64() {
261                        let abs = off + base as u64;
262                        t.raw_value = Value::U32(abs as u32);
263                        t.print_value = abs.to_string();
264                    }
265                }
266            }
267        }
268        Ok(tags)
269    }
270
271    /// ExifTool raises ExifByteOrder at file level, not from IFD0.
272    fn exif_byte_order_tag(byte_order: ByteOrderMark) -> Tag {
273        let bo_str = match byte_order {
274            ByteOrderMark::LittleEndian => "Little-endian (Intel, II)",
275            ByteOrderMark::BigEndian => "Big-endian (Motorola, MM)",
276        };
277        Tag {
278            id: TagId::Text("ExifByteOrder".to_string()),
279            name: "ExifByteOrder".to_string(),
280            description: "Exif Byte Order".to_string(),
281            group: TagGroup {
282                family0: "File".to_string(),
283                family1: "File".to_string(),
284                family2: "Image".to_string(),
285                family3: "Main".into(),
286            },
287            raw_value: Value::String(bo_str.to_string()),
288            print_value: bo_str.to_string(),
289            priority: 0,
290        }
291    }
292
293    fn read_inner(data: &[u8], exif_base: usize) -> Result<Vec<Tag>> {
294        let header = parse_tiff_header(data)?;
295        let mut tags = Vec::new();
296
297        // Emit ExifByteOrder tag
298        tags.push(Self::exif_byte_order_tag(header.byte_order));
299
300        // Detect CR2: "CR" at offset 8 in TIFF data
301        let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
302
303        // Read IFD0 (main image)
304        Self::read_ifd(data, &header, header.ifd0_offset, "IFD0", &mut tags)?;
305
306        // An ARW or SR2 names its IFD0 0x0201/0x0202 pair PreviewImageStart and
307        // PreviewImageLength (done as they are read), and ExifTool extracts the
308        // image they point at, exactly as it does for a CR2.
309        let preview_in_ifd0 = is_cr2 || matches!(tiff_type().as_str(), "ARW" | "SR2");
310
311        // For CR2 files, rename IFD0 StripOffsets→PreviewImageStart and
312        // StripByteCounts→PreviewImageLength, then construct PreviewImage.
313        if preview_in_ifd0 {
314            // Rename tags in-place
315            for tag in tags.iter_mut() {
316                if tag.group.family1 == "IFD0" {
317                    if tag.name == "StripOffsets" {
318                        tag.name = "PreviewImageStart".to_string();
319                        tag.description = "Preview Image Start".to_string();
320                        tag.id = TagId::Text("PreviewImageStart".to_string());
321                    } else if tag.name == "StripByteCounts" {
322                        tag.name = "PreviewImageLength".to_string();
323                        tag.description = "Preview Image Length".to_string();
324                        tag.id = TagId::Text("PreviewImageLength".to_string());
325                    }
326                }
327            }
328            // Construct PreviewImage from PreviewImageStart + PreviewImageLength
329            let preview_start = tags
330                .iter()
331                .find(|t| t.name == "PreviewImageStart" && t.group.family1 == "IFD0")
332                .and_then(|t| t.raw_value.as_u64())
333                .map(|v| v as usize);
334            let preview_len = tags
335                .iter()
336                .find(|t| t.name == "PreviewImageLength" && t.group.family1 == "IFD0")
337                .and_then(|t| t.raw_value.as_u64())
338                .map(|v| v as usize);
339            if let (Some(start), Some(len)) = (preview_start, preview_len) {
340                if len > 0 {
341                    // The tag reports the length the file declares. A preview
342                    // that runs past the end of what we hold still exists as a
343                    // tag -- ExifTool lists it from the pair, not from the
344                    // bytes -- so only the payload is trimmed to what is there.
345                    let img_data = data
346                        .get(start..start + len)
347                        .or_else(|| data.get(start..))
348                        .unwrap_or_default()
349                        .to_vec();
350                    let pv = format!("(Binary data {} bytes, use -b option to extract)", len);
351                    tags.push(Tag {
352                        id: TagId::Text("PreviewImage".to_string()),
353                        name: "PreviewImage".to_string(),
354                        description: "Preview Image".to_string(),
355                        group: TagGroup {
356                            family0: "EXIF".to_string(),
357                            family1: "IFD0".to_string(),
358                            family2: "Preview".to_string(),
359                            family3: "Main".into(),
360                        },
361                        raw_value: Value::Binary(img_data),
362                        print_value: pv,
363                        priority: 0,
364                    });
365                }
366            }
367        }
368
369        // ExifTool stores Software as it reads it (`DataMember => 'Software'`,
370        // Exif.pm:905) because MakerNote conditions test it -- Sony reads
371        // ShutterCount2 from a different offset on an ILCE-9 running v5 or v6.
372        if let Some(sw) = tags.iter().find(|t| t.name == "Software") {
373            set_software(sw.print_value.trim_end());
374        }
375
376        // Extract Make + Model for MakerNotes detection and sub-table dispatch
377        if let Some(mk) = tags.iter().find(|t| t.name == "Make") {
378            set_make(mk.print_value.trim_end());
379        }
380        let make = tags
381            .iter()
382            .find(|t| t.name == "Make")
383            .map(|t| t.print_value.clone())
384            .unwrap_or_default();
385
386        let model = tags
387            .iter()
388            .find(|t| t.name == "Model")
389            .map(|t| t.print_value.clone())
390            .unwrap_or_default();
391
392        // Store model for sub-table dispatch
393        let make_and_model = if model.is_empty() {
394            make.clone()
395        } else {
396            model
397        };
398
399        // Find and parse MakerNotes
400        // Look for the MakerNote tag (0x927C) that was stored as Undefined
401        let mn_info: Option<(usize, usize)> = {
402            // Re-scan ExifIFD for MakerNote offset/size
403            let mut result = None;
404            Self::find_makernote(data, &header, &mut result);
405            result
406        };
407
408        if let Some((mn_offset, mn_size)) = mn_info {
409            let mn_tags = crate::metadata::makernotes::parse_makernotes_exif_base(
410                data,
411                mn_offset,
412                mn_size,
413                &make,
414                &make_and_model,
415                header.byte_order,
416                exif_base,
417            );
418            // The parsed tags take the raw MakerNote tag's place in the stream.
419            // ExifTool descends into the maker note the moment ProcessExif reaches
420            // 0x927c inside the ExifIFD (Exif.pm:6113 `ProcessDirectory` on the
421            // SubDirectory), so every maker-note tag is stored BEFORE the walk
422            // resumes and reaches IFD1. That order decides every priority-0 tie:
423            // in a JPEG both `PreviewIFD` and `IFD1` are in LOW_PRIORITY_DIR
424            // (ExifTool.pm:4368 and ProcessJPEG :7317), so the tie is first-wins
425            // and the PreviewIFD copy has to be seen first.
426            // In Perl ExifTool, MakerNotes tags with equal/higher priority overwrite EXIF tags.
427            // Tags in the EXIF-primary list: EXIF wins (skip MakerNotes duplicate).
428            // Other tags: MakerNotes wins (remove EXIF version, add MakerNotes version).
429            // The placeholder is still in `tags` here, so its index is looked up
430            // at splice time — the EXIF-duplicate pruning below runs first and
431            // would invalidate an index taken any earlier.
432            let splice_in = |tags: &mut Vec<Tag>, new: Vec<Tag>| match tags
433                .iter()
434                .position(|t| t.name == "MakerNote")
435            {
436                Some(i) => {
437                    tags.splice(i..=i, new);
438                }
439                None => tags.extend(new),
440            };
441            if keep_duplicates() {
442                // Duplicates are kept: ExifTool reports the EXIF and the maker-note
443                // copy of a tag side by side (e.g. FujiFilm Contrast/Saturation/
444                // Sharpness or Panasonic TextStamp under -ee).
445                splice_in(&mut tags, mn_tags);
446            } else {
447                // Tags where EXIF takes priority over MakerNotes (structural/authoritative EXIF)
448                let exif_primary: &[&str] = EXIF_PRIMARY_TAGS;
449                // Only a maker-note tag ExifTool would let win outright removes
450                // the EXIF duplicate here. A tag whose source table states
451                // `PRIORITY => 0` — Minolta::CameraSettings (Minolta.pm:974) and
452                // its siblings — does not: FoundTag promotes the stored EXIF tag
453                // to 1 first (ExifTool.pm:9544-9551), so it keeps the name unless
454                // it is itself demoted, and only the central arbitration knows
455                // that. Leaving both instances in place is what lets ExifIFD keep
456                // MeteringMode while the maker note takes WhiteBalance, whose
457                // 0xa403 carries `Priority => 0` "to keep this WhiteBalance from
458                // overriding the MakerNotes WhiteBalance" (Exif.pm:2877-2880).
459                // A `PreviewIFD` tag cannot either: ExifTool.pm:4368 initialises
460                // `LOW_PRIORITY_DIR = { PreviewIFD => 1 }`, and `Nikon::PreviewIFD`
461                // says so itself (Nikon.pm:5391, "these tags are priority 0 by
462                // default because PreviewIFD is flagged in LOW_PRIORITY_DIR").
463                let mn_name_set: std::collections::HashSet<String> = mn_tags
464                    .iter()
465                    .filter(|t| {
466                        t.priority_rank() >= 0
467                            && t.priority != crate::tag::PRIORITY_EXPLICIT_ZERO
468                            && t.group.family1 != "PreviewIFD"
469                    })
470                    .map(|t| t.name.clone())
471                    .collect();
472                let exif_has: std::collections::HashSet<String> =
473                    tags.iter().map(|t| t.name.clone()).collect();
474                // Remove EXIF non-primary tags when MakerNotes provides them (MakerNotes wins)
475                tags.retain(|t| {
476                    !mn_name_set.contains(&t.name) || exif_primary.contains(&t.name.as_str())
477                });
478                // Add MakerNotes tags, but skip EXIF-primary tags that EXIF already provides.
479                // Exception: a few maker notes carry a more precise authoritative value
480                // (e.g. Kodak FNumber/ExposureTime) that ExifTool reports over EXIF — keep
481                // those so the later precedence pass can promote them.
482                let kept: Vec<Tag> = mn_tags
483                    .into_iter()
484                    .filter(|mn_tag| {
485                        let authoritative = mn_tag.group.family1 == "Kodak"
486                            && matches!(mn_tag.name.as_str(), "FNumber" | "ExposureTime");
487                        // EXIF wins — don't add the MakerNotes version. A
488                        // `PreviewIFD` tag is exempt in both directions: it is
489                        // priority 0 (ExifTool.pm:4368), so the arbitration pass
490                        // in `exiftool` already lets IFD0 beat it, while against
491                        // the equally-demoted IFD1 of a JPEG (ProcessJPEG,
492                        // ExifTool.pm:7317) the tie is first-wins and the
493                        // PreviewIFD copy is the one ExifTool keeps.
494                        authoritative
495                            || mn_tag.group.family1 == "PreviewIFD"
496                            || !exif_primary.contains(&mn_tag.name.as_str())
497                            || !exif_has.contains(&mn_tag.name)
498                    })
499                    .collect();
500                splice_in(&mut tags, kept);
501            }
502        }
503
504        // Sony hangs a private IFD off the same tag, and reaching it does not
505        // depend on whether a MakerNote was found: a raw file has both.
506        if make.to_ascii_uppercase().starts_with("SONY") {
507            Self::parse_sony_sr2(data, &header, &mut tags);
508        }
509
510        // DNG PrivateData (0xC634): parse Adobe MakN for MakerNotes if no MakerNote found
511        if mn_info.is_none() {
512            // Scan for DNGPrivateData in tags — look for "Adobe\0" header
513            // Find the offset from the tag value (stored as binary/undefined)
514            Self::parse_dng_private_data(data, &header, &make, &make_and_model, &mut tags);
515        }
516
517        // Parse IPTC data embedded in TIFF (tag 0x83BB "IPTC-NAA")
518        // The raw tag stores IPTC data as undefined bytes or a list of u32 values
519        {
520            let iptc_data: Option<Vec<u8>> =
521                tags.iter().find(|t| t.name == "IPTC-NAA").and_then(|t| {
522                    match &t.raw_value {
523                        Value::Undefined(bytes) => Some(bytes.clone()),
524                        Value::Binary(bytes) => Some(bytes.clone()),
525                        Value::List(items) => {
526                            // IPTC-NAA stored as uint32 list - convert back to bytes (big-endian)
527                            let mut bytes = Vec::with_capacity(items.len() * 4);
528                            for item in items {
529                                if let Value::U32(v) = item {
530                                    bytes.extend_from_slice(&v.to_be_bytes())
531                                }
532                            }
533                            if bytes.is_empty() {
534                                None
535                            } else {
536                                Some(bytes)
537                            }
538                        }
539                        _ => None,
540                    }
541                });
542
543            if let Some(iptc_bytes) = iptc_data {
544                // Compute MD5 of the raw IPTC data for CurrentIPTCDigest
545                let md5_hex = crate::md5::md5_hex(&iptc_bytes);
546
547                if let Ok(iptc_tags) = crate::metadata::IptcReader::read(&iptc_bytes) {
548                    // Replace raw IPTC-NAA tag with parsed IPTC tags
549                    tags.retain(|t| t.name != "IPTC-NAA");
550                    tags.extend(iptc_tags);
551                }
552
553                // Add CurrentIPTCDigest tag
554                tags.push(crate::tag::Tag {
555                    id: crate::tag::TagId::Text("CurrentIPTCDigest".into()),
556                    name: "CurrentIPTCDigest".into(),
557                    description: "Current IPTC Digest".into(),
558                    group: crate::tag::TagGroup {
559                        family0: "IPTC".into(),
560                        family1: "IPTC".into(),
561                        family2: "Other".into(),
562                        family3: "Main".into(),
563                    },
564                    raw_value: Value::String(md5_hex.clone()),
565                    print_value: md5_hex,
566                    priority: 0,
567                });
568            }
569        }
570
571        // Parse ICC_Profile data embedded in TIFF (tag 0x8773)
572        {
573            let icc_data: Option<Vec<u8>> =
574                tags.iter()
575                    .find(|t| t.name == "ICC_Profile")
576                    .and_then(|t| match &t.raw_value {
577                        Value::Undefined(bytes) => Some(bytes.clone()),
578                        Value::Binary(bytes) => Some(bytes.clone()),
579                        _ => None,
580                    });
581
582            if let Some(icc_bytes) = icc_data {
583                if let Ok(icc_tags) = crate::formats::icc::read_icc(&icc_bytes) {
584                    // Replace raw ICC_Profile tag with parsed ICC tags
585                    tags.retain(|t| t.name != "ICC_Profile");
586                    tags.extend(icc_tags);
587                }
588            }
589        }
590
591        // Process GeoTIFF key directory if present
592        process_geotiff_keys(&mut tags);
593
594        // Final deduplication: within MakerNotes, if the same tag name appears multiple times
595        // (e.g., from different sub-tables), keep the last occurrence.
596        // Only deduplicate MakerNotes tags (family0 == "MakerNotes") to avoid affecting
597        // structural EXIF/IFD tags.
598        //
599        // This is ExifTool collapsing its name-keyed VALUE hash, so it must not run
600        // when the Duplicates option is on: a maker note that genuinely defines the
601        // same name at several tag IDs (Panasonic TextStamp at 0x3b, 0x3e, 0x8008
602        // and 0x8009, BabyAge at 0x33 and 0x8010 — Panasonic.pm:727, 789, 811,
603        // 1570, 1576, 1581) is then reported once per ID.
604        {
605            // With duplicates kept, a name is only collapsed when the SAME tag ID
606            // produced it twice — that is our own sub-table reading a tag the main
607            // table already read, never two IDs the maker note really defines
608            // under one name.
609            let by_id = keep_duplicates();
610            if tags.iter().any(|t| t.group.family0 == "MakerNotes") {
611                // Replay FoundTag's comparison rather than simply keeping the
612                // last occurrence: an incoming tag takes the name only when its
613                // priority is >= the stored one's, and a stored 0 is promoted to
614                // 1 first (ExifTool.pm:9544-9560). A sub-table that states
615                // `PRIORITY => 0` — every Canon::CameraInfo* (Canon.pm:3162 and
616                // siblings) — therefore does not displace the value an earlier
617                // sub-table stored, which is how Canon::ShotInfo keeps
618                // WhiteBalance against CameraInfo1DmkIII's.
619                let eff = |t: &Tag| -> i32 {
620                    if t.priority == crate::tag::PRIORITY_EXPLICIT_ZERO {
621                        0
622                    } else if t.priority == 0 {
623                        1
624                    } else {
625                        t.priority
626                    }
627                };
628                let promoted = |p: i32| if p == 0 { 1 } else { p };
629                // With duplicates kept, two instances of a name are the same
630                // tag only when they come from the same directory as well as
631                // the same id: an ARW holds thirteen SR2DataIFDs, each with a
632                // ColorMode of its own, and ExifTool reports all thirteen.
633                let mut winner: std::collections::HashMap<
634                    (&str, Option<&TagId>, Option<&str>),
635                    usize,
636                > = std::collections::HashMap::new();
637                let mut keep = vec![true; tags.len()];
638                for (i, t) in tags.iter().enumerate() {
639                    if t.group.family0 != "MakerNotes" {
640                        continue;
641                    }
642                    let key = (
643                        t.name.as_str(),
644                        if by_id { Some(&t.id) } else { None },
645                        if by_id {
646                            Some(t.group.family1.as_str())
647                        } else {
648                            None
649                        },
650                    );
651                    match winner.get(&key).copied() {
652                        None => {
653                            winner.insert(key, i);
654                        }
655                        Some(w) => {
656                            if eff(t) >= promoted(eff(&tags[w])) {
657                                keep[w] = false;
658                                winner.insert(key, i);
659                            } else {
660                                keep[i] = false;
661                            }
662                        }
663                    }
664                }
665                let mut iter = keep.iter();
666                tags.retain(|_| *iter.next().unwrap_or(&true));
667            }
668        }
669
670        // GPS.pm:17-21 `%coordConv` — GPSLatitude and GPSLongitude print through
671        // `ToDMS($self, $val, 1)`, which normalises fractional minutes into
672        // seconds and does NOT append the hemisphere reference. The ref belongs to
673        // the Composite of the same name (GPS.pm:367-405), whose PrintConv is
674        // `ToDMS($self, $val, 1, "N"/"E")`; see `composite::gps_coordinates`.
675        for coord in ["GPSLatitude", "GPSLongitude"] {
676            if let Some(t) = tags.iter_mut().find(|t| t.name == coord) {
677                let parts: Vec<f64> = t
678                    .raw_value
679                    .to_display_string()
680                    .split_whitespace()
681                    .filter_map(|s| s.parse::<f64>().ok())
682                    .collect();
683                if parts.len() == 3 {
684                    let dec = parts[0] + parts[1] / 60.0 + parts[2] / 3600.0;
685                    let deg = dec.floor();
686                    let rem = (dec - deg) * 60.0;
687                    let min = rem.floor();
688                    let sec = (rem - min) * 60.0;
689                    t.print_value = format!("{} deg {}' {:.2}\"", deg as i64, min as i64, sec);
690                }
691            }
692            // Undefined coordinate rationals (0/0 0/0 0/0) yield an empty value.
693            if let Some(t) = tags.iter_mut().find(|t| t.name == coord) {
694                if t.print_value.split_whitespace().all(|p| p == "undef") {
695                    t.print_value = String::new();
696                }
697            }
698        }
699
700        // ExifTool uses the full-resolution sub-IFD (SubfileType = "Full-resolution
701        // image") for the primary image dimensions. When such a sub-IFD exists (e.g. the
702        // real raw image in a DNG/NEF whose IFD0 is a small reduced-resolution preview),
703        // promote its ImageWidth/ImageHeight to the front so they win first-by-name and
704        // feed the ImageSize/Megapixels composites.
705        let fullres_group = tags
706            .iter()
707            .find(|t| {
708                t.name == "SubfileType"
709                    && t.print_value == "Full-resolution image"
710                    && t.group.family1 != "IFD0"
711            })
712            .map(|t| t.group.family1.clone());
713        if let Some(group) = fullres_group {
714            for dim in ["ImageHeight", "ImageWidth"] {
715                if let Some(pos) = tags
716                    .iter()
717                    .position(|t| t.name == dim && t.group.family1 == group)
718                {
719                    let t = tags.remove(pos);
720                    tags.insert(0, t);
721                }
722            }
723        }
724
725        Ok(tags)
726    }
727
728    /// Find MakerNote (tag 0x927C) offset and size in ExifIFD.
729    /// Read Sony's private IFD, hung off DNGPrivateData (0xC634).
730    ///
731    /// Sony writes the tag as a single int32u whose value is where that IFD
732    /// starts, rather than as the byte block Adobe puts there.
733    fn parse_sony_sr2(data: &[u8], header: &TiffHeader, tags: &mut Vec<Tag>) {
734        let ifd0 = header.ifd0_offset as usize;
735        if ifd0 + 2 > data.len() {
736            return;
737        }
738        let count = read_u16(data, ifd0, header.byte_order) as usize;
739        for i in 0..count {
740            let e = ifd0 + 2 + i * 12;
741            if e + 12 > data.len() {
742                break;
743            }
744            if read_u16(data, e, header.byte_order) != 0xC634 {
745                continue;
746            }
747            let private_offset = read_u32(data, e + 8, header.byte_order) as usize;
748            let le = header.byte_order == ByteOrderMark::LittleEndian;
749            tags.extend(crate::metadata::sony_sr2::read(data, private_offset, le));
750            return;
751        }
752    }
753
754    /// Parse DNG PrivateData (0xC634) to extract embedded MakerNotes
755    fn parse_dng_private_data(
756        data: &[u8],
757        header: &TiffHeader,
758        make: &str,
759        model: &str,
760        tags: &mut Vec<Tag>,
761    ) {
762        // Scan IFD0 for tag 0xC634
763        let ifd0_offset = header.ifd0_offset as usize;
764        if ifd0_offset + 2 > data.len() {
765            return;
766        }
767        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
768        let entries_start = ifd0_offset + 2;
769        for i in 0..entry_count {
770            let eoff = entries_start + i * 12;
771            if eoff + 12 > data.len() {
772                break;
773            }
774            let tag = read_u16(data, eoff, header.byte_order);
775            if tag == 0xC634 {
776                let dtype = read_u16(data, eoff + 2, header.byte_order);
777                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
778                let elem_size = match dtype {
779                    1 | 7 => 1,
780                    _ => 0,
781                };
782                let total = elem_size * count;
783                if total < 14 {
784                    continue;
785                }
786                let off = read_u32(data, eoff + 8, header.byte_order) as usize;
787                if off + total > data.len() {
788                    continue;
789                }
790                let pdata = &data[off..off + total];
791                // Parse Adobe DNGPrivateData: "Adobe\0" + blocks
792                if !pdata.starts_with(b"Adobe\0") {
793                    continue;
794                }
795                let mut bpos = 6;
796                while bpos + 8 <= pdata.len() {
797                    let btag = &pdata[bpos..bpos + 4];
798                    let bsize = u32::from_be_bytes([
799                        pdata[bpos + 4],
800                        pdata[bpos + 5],
801                        pdata[bpos + 6],
802                        pdata[bpos + 7],
803                    ]) as usize;
804                    bpos += 8;
805                    if bpos + bsize > pdata.len() {
806                        break;
807                    }
808                    if btag == b"MakN" && bsize > 6 {
809                        let mn_block = &pdata[bpos..bpos + bsize];
810                        let mn_bo = if &mn_block[0..2] == b"II" {
811                            ByteOrderMark::LittleEndian
812                        } else {
813                            ByteOrderMark::BigEndian
814                        };
815                        let mut mn_start = 6; // skip byte order + original offset
816                                              // Hack for extra 12 bytes in MakN header (Adobe Camera Raw bug)
817                        if bsize >= 18 && &mn_block[6..10] == b"\0\0\0\x01" {
818                            mn_start += 12;
819                        }
820                        if mn_start < bsize {
821                            // Emit MakerNoteByteOrder
822                            let mn_bo_str = if mn_bo == ByteOrderMark::LittleEndian {
823                                "Little-endian (Intel, II)"
824                            } else {
825                                "Big-endian (Motorola, MM)"
826                            };
827                            tags.push(Tag {
828                                id: TagId::Text("MakerNoteByteOrder".into()),
829                                name: "MakerNoteByteOrder".into(),
830                                description: "Maker Note Byte Order".into(),
831                                group: TagGroup {
832                                    family0: "File".into(),
833                                    family1: "File".into(),
834                                    family2: "Image".into(),
835                                    family3: "Main".into(),
836                                },
837                                raw_value: Value::String(mn_bo_str.into()),
838                                print_value: mn_bo_str.into(),
839                                priority: 0,
840                            });
841                            // Canon MakerNotes have a TIFF footer with the original offset.
842                            // Sub-table value_offsets are relative to the original file.
843                            // We need to pass the full DNG data so offsets resolve correctly.
844                            let mn_data_in_block = &mn_block[mn_start..];
845                            let mn_abs_offset = off + (bpos - 8 + 8) + mn_start; // absolute offset in DNG file
846                                                                                 // Check for Canon TIFF footer (last 8 bytes)
847                            let fix_base = if mn_data_in_block.len() > 8 {
848                                let footer = &mn_data_in_block[mn_data_in_block.len() - 8..];
849                                if (footer[0..2] == *b"II" || footer[0..2] == *b"MM")
850                                    && (footer[2..4] == *b"\x2a\x00"
851                                        || footer[2..4] == *b"\x00\x2a")
852                                {
853                                    let old_off = if footer[0] == b'I' {
854                                        u32::from_le_bytes([
855                                            footer[4], footer[5], footer[6], footer[7],
856                                        ])
857                                    } else {
858                                        u32::from_be_bytes([
859                                            footer[4], footer[5], footer[6], footer[7],
860                                        ])
861                                    } as usize;
862                                    if old_off > 0 && mn_abs_offset > old_off {
863                                        mn_abs_offset as isize - old_off as isize
864                                    } else {
865                                        0
866                                    }
867                                } else {
868                                    0
869                                }
870                            } else {
871                                0
872                            };
873
874                            let mn_tags = if fix_base != 0 {
875                                // Pass full DNG data with corrected offset and base fix
876                                crate::metadata::makernotes::parse_makernotes_with_base(
877                                    data,
878                                    mn_abs_offset,
879                                    mn_data_in_block.len(),
880                                    make,
881                                    model,
882                                    mn_bo,
883                                    fix_base,
884                                )
885                            } else {
886                                crate::metadata::makernotes::parse_makernotes(
887                                    mn_data_in_block,
888                                    0,
889                                    mn_data_in_block.len(),
890                                    make,
891                                    model,
892                                    mn_bo,
893                                )
894                            };
895                            // DNG MakerNote tags that Perl doesn't emit (conditions/Unknown/offset issues)
896                            let dng_suppress = [
897                                "AESetting",
898                                "CameraISO",
899                                "ImageStabilization",
900                                "SpotMeteringMode",
901                                "RawJpgSize",
902                                "Warning",
903                            ];
904                            for mn_tag in mn_tags {
905                                if dng_suppress.contains(&mn_tag.name.as_str()) {
906                                    continue;
907                                }
908                                // No name-collision filtering here: ExifTool
909                                // extracts the maker note in full and lets the
910                                // duplicate arbitration pick a winner later,
911                                // which is the only step the Duplicates option
912                                // (and therefore -ee) turns off.
913                                tags.push(mn_tag);
914                            }
915                        }
916                    }
917                    bpos += bsize;
918                }
919                break;
920            }
921        }
922    }
923
924    fn find_makernote(data: &[u8], header: &TiffHeader, result: &mut Option<(usize, usize)>) {
925        // First find ExifIFD offset from IFD0
926        let ifd0_offset = header.ifd0_offset as usize;
927        if ifd0_offset + 2 > data.len() {
928            return;
929        }
930        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
931        let entries_start = ifd0_offset + 2;
932
933        for i in 0..entry_count {
934            let eoff = entries_start + i * 12;
935            if eoff + 12 > data.len() {
936                break;
937            }
938            let tag = read_u16(data, eoff, header.byte_order);
939            if tag == 0x8769 {
940                // ExifIFD pointer
941                let exif_offset = read_u32(data, eoff + 8, header.byte_order) as usize;
942                Self::find_makernote_in_ifd(data, header, exif_offset, result);
943                break;
944            }
945        }
946    }
947
948    fn find_makernote_in_ifd(
949        data: &[u8],
950        header: &TiffHeader,
951        ifd_offset: usize,
952        result: &mut Option<(usize, usize)>,
953    ) {
954        if ifd_offset + 2 > data.len() {
955            return;
956        }
957        let entry_count = read_u16(data, ifd_offset, header.byte_order) as usize;
958        let entries_start = ifd_offset + 2;
959
960        for i in 0..entry_count {
961            let eoff = entries_start + i * 12;
962            if eoff + 12 > data.len() {
963                break;
964            }
965            let tag = read_u16(data, eoff, header.byte_order);
966            if tag == 0x927C {
967                let data_type = read_u16(data, eoff + 2, header.byte_order);
968                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
969                let type_size = match data_type {
970                    1 | 2 | 6 | 7 => 1,
971                    3 | 8 => 2,
972                    4 | 9 | 11 | 13 => 4,
973                    5 | 10 | 12 => 8,
974                    _ => 1,
975                };
976                let total_size = type_size * count;
977
978                if total_size <= 4 {
979                    // Inline - too small for real MakerNotes
980                    break;
981                }
982                let offset = read_u32(data, eoff + 8, header.byte_order) as usize;
983                if offset + total_size <= data.len() {
984                    *result = Some((offset, total_size));
985                }
986                break;
987            }
988        }
989    }
990
991    /// Parse EXIF data from a byte slice with an explicit byte order and offset.
992    fn read_ifd(
993        data: &[u8],
994        header: &TiffHeader,
995        offset: u32,
996        ifd_name: &str,
997        tags: &mut Vec<Tag>,
998    ) -> Result<Option<u32>> {
999        let offset = offset as usize;
1000        if offset + 2 > data.len() {
1001            return Err(Error::InvalidExif(format!(
1002                "{} offset {} beyond data length {}",
1003                ifd_name,
1004                offset,
1005                data.len()
1006            )));
1007        }
1008
1009        let entry_count = read_u16(data, offset, header.byte_order) as usize;
1010        let entries_start = offset + 2;
1011        let _entries_end = entries_start + entry_count * 12;
1012
1013        // Validate: at minimum, first entry must fit
1014        if entries_start + 12 > data.len() && entry_count > 0 {
1015            return Err(Error::InvalidExif(format!(
1016                "{} entries extend beyond data (need {}, have {})",
1017                ifd_name,
1018                entries_start + 12,
1019                data.len()
1020            )));
1021        }
1022        // Clamp entry count if IFD extends beyond data
1023        let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
1024        let entries_end = entries_start + entry_count * 12;
1025
1026        for i in 0..entry_count {
1027            let entry_offset = entries_start + i * 12;
1028            let entry = parse_ifd_entry(data, entry_offset, header.byte_order);
1029
1030            // Check for sub-IFDs (ExifIFD, GPS, Interop)
1031            match entry.tag {
1032                0x8769 => {
1033                    // ExifIFD
1034                    let sub_offset = entry.value_offset;
1035                    if (sub_offset as usize) < data.len() {
1036                        let _ = Self::read_ifd(data, header, sub_offset, "ExifIFD", tags);
1037                    }
1038                    continue;
1039                }
1040                0x4748 => {
1041                    // StitchInfo, a binary block Microsoft writes in IFD0
1042                    // (Exif.pm:1534-1540), little-endian whatever the file is.
1043                    let total =
1044                        type_size(entry.data_type).map_or(0, |sz| sz * entry.count as usize);
1045                    let block: Option<&[u8]> = if total <= 4 {
1046                        Some(&entry.inline_data[..total.min(4)])
1047                    } else {
1048                        let off = entry.value_offset as usize;
1049                        data.get(off..off + total)
1050                    };
1051                    if let Some(block) = block {
1052                        let mut dm = crate::tags::binary_tables_generated::State::new();
1053                        tags.extend(crate::tags::binary_tables_generated::decode(
1054                            "Microsoft::Stitch",
1055                            block,
1056                            "",
1057                            "",
1058                            ByteOrderMark::LittleEndian,
1059                            "",
1060                            "",
1061                            &mut dm,
1062                        ));
1063                    }
1064                    continue;
1065                }
1066                0x8825 => {
1067                    // GPS IFD
1068                    let sub_offset = entry.value_offset;
1069                    if (sub_offset as usize) < data.len() {
1070                        let _ = Self::read_ifd(data, header, sub_offset, "GPS", tags);
1071                    }
1072                    continue;
1073                }
1074                0xA005 => {
1075                    // Interop IFD
1076                    let sub_offset = entry.value_offset;
1077                    if (sub_offset as usize) < data.len() {
1078                        let _ = Self::read_ifd(data, header, sub_offset, "InteropIFD", tags);
1079                    }
1080                    continue;
1081                }
1082                // PrintIM tag: extract version from "PrintIM" + 4-byte version
1083                0xC4A5 => {
1084                    let total_size = match entry.data_type {
1085                        1 | 2 | 6 | 7 => entry.count as usize,
1086                        _ => 0,
1087                    };
1088                    if total_size >= 12 {
1089                        let off = entry.value_offset as usize;
1090                        if off + 12 <= data.len() && &data[off..off + 7] == b"PrintIM" {
1091                            // "PrintIM\0" is 8 bytes; the 4-byte version follows.
1092                            let ver =
1093                                crate::encoding::decode_utf8_or_latin1(&data[off + 8..off + 12])
1094                                    .trim_end_matches('\0')
1095                                    .to_string();
1096                            tags.push(Tag {
1097                                id: TagId::Text("PrintIMVersion".into()),
1098                                name: "PrintIMVersion".into(),
1099                                description: "PrintIM Version".into(),
1100                                group: TagGroup {
1101                                    family0: "PrintIM".into(),
1102                                    family1: "PrintIM".into(),
1103                                    family2: "Printing".into(),
1104                                    family3: "Main".into(),
1105                                },
1106                                raw_value: Value::String(ver.clone()),
1107                                print_value: ver,
1108                                priority: 0,
1109                            });
1110                        }
1111                    }
1112                    continue; // Suppress raw PrintIM tag
1113                }
1114                // GPSAltitude with a 0/0 rational is NOT suppressed: GPS.pm gives a
1115                // zero-denominator rational the ValueConv string "undef", which the
1116                // PrintConv `$val =~ /^(inf|undef)$/ ? $val : "$val m"` passes
1117                // through unchanged (GPS.pm:119-125). So it prints "undef", exactly
1118                // like the sibling GPSSpeed rational in the same file.
1119                // Exif.pm 0x201: the name depends on where the tag is and what
1120                // kind of TIFF this is. In an ARW or an SR2, IFD0 holds the
1121                // preview, not a thumbnail.
1122                0x0201 | 0x0202
1123                    if ifd_name == "IFD0" && matches!(tiff_type().as_str(), "ARW" | "SR2") =>
1124                {
1125                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
1126                        let (name, desc) = if entry.tag == 0x0201 {
1127                            ("PreviewImageStart", "Preview Image Start")
1128                        } else {
1129                            ("PreviewImageLength", "Preview Image Length")
1130                        };
1131                        let pv = val.to_display_string();
1132                        tags.push(Tag {
1133                            id: TagId::Numeric(entry.tag),
1134                            name: name.into(),
1135                            description: desc.into(),
1136                            group: TagGroup {
1137                                family0: "EXIF".into(),
1138                                family1: ifd_name.to_string(),
1139                                family2: "Image".into(),
1140                                family3: "Main".into(),
1141                            },
1142                            raw_value: val,
1143                            print_value: pv,
1144                            priority: 0,
1145                        });
1146                    }
1147                    continue;
1148                }
1149                // In SubIFD, tag 0x0201 = JpgFromRawStart (JPEG preview offset)
1150                0x0201 if ifd_name.starts_with("SubIFD") => {
1151                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
1152                        let pv = val.to_display_string();
1153                        tags.push(Tag {
1154                            id: TagId::Numeric(entry.tag),
1155                            name: "JpgFromRawStart".into(),
1156                            description: "Jpg From Raw Start".into(),
1157                            group: TagGroup {
1158                                family0: "EXIF".into(),
1159                                family1: ifd_name.to_string(),
1160                                family2: "Image".into(),
1161                                family3: "Main".into(),
1162                            },
1163                            raw_value: val,
1164                            print_value: pv,
1165                            priority: 0,
1166                        });
1167                    }
1168                    continue;
1169                }
1170                // In SubIFD, tag 0x0202 = JpgFromRawLength (JPEG preview byte count)
1171                0x0202 if ifd_name.starts_with("SubIFD") => {
1172                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
1173                        let pv = val.to_display_string();
1174                        tags.push(Tag {
1175                            id: TagId::Numeric(entry.tag),
1176                            name: "JpgFromRawLength".into(),
1177                            description: "Jpg From Raw Length".into(),
1178                            group: TagGroup {
1179                                family0: "EXIF".into(),
1180                                family1: ifd_name.to_string(),
1181                                family2: "Image".into(),
1182                                family3: "Main".into(),
1183                            },
1184                            raw_value: val,
1185                            print_value: pv,
1186                            priority: 0,
1187                        });
1188                    }
1189                    continue;
1190                }
1191                // SubIFD pointer (0x014A): follow to read SubIFD entries
1192                0x014A if ifd_name == "IFD0" => {
1193                    // Read SubIFD offset(s) — may be a single uint32 or array
1194                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
1195                        let offsets: Vec<u32> = match &val {
1196                            Value::U32(v) => vec![*v],
1197                            Value::List(items) => items
1198                                .iter()
1199                                .filter_map(|v| {
1200                                    if let Value::U32(o) = v {
1201                                        Some(*o)
1202                                    } else {
1203                                        None
1204                                    }
1205                                })
1206                                .collect(),
1207                            _ => vec![],
1208                        };
1209                        for (idx, &off) in offsets.iter().enumerate() {
1210                            if (off as usize) < data.len() {
1211                                // ExifTool leaves the first SubIFD unnumbered and
1212                                // numbers the rest from 1.
1213                                let sub_name = if idx == 0 {
1214                                    "SubIFD".to_string()
1215                                } else {
1216                                    format!("SubIFD{}", idx)
1217                                };
1218                                let before_idx = tags.len();
1219                                let _ = Self::read_ifd(data, header, off, &sub_name, tags);
1220
1221                                // Check if this SubIFD has JPEG compression
1222                                let is_jpeg = tags[before_idx..].iter().any(|t| {
1223                                    t.name == "Compression"
1224                                        && (t.print_value.contains("JPEG")
1225                                            || t.raw_value.as_u64() == Some(6))
1226                                });
1227
1228                                // Exif.pm makes 0x111 and 0x117 conditional arrays: the
1229                                // StripOffsets / StripByteCounts branch (Exif.pm:638 and
1230                                // Exif.pm:738) is skipped when
1231                                //   `$$self{TIFF_TYPE} =~ /^(DNG|TIFF)$/ and
1232                                //    $$self{Compression} eq '7' and $$self{SubfileType} ne '0'`
1233                                // and the next branch names the tag PreviewImageStart /
1234                                // PreviewImageLength -- or, in SubIFD2, JpgFromRawStart /
1235                                // JpgFromRawLength (Exif.pm:664/761, 675/771). It is a RENAME: such
1236                                // a directory has no StripOffsets at all, which is why
1237                                // ExifTool reports DNG.dng's IFD0 StripOffsets (13470) as
1238                                // primary even though SubIFD2 comes later in the file.
1239                                let is_renamed_offset_pair = tags[before_idx..].iter().any(|t| {
1240                                    t.name == "Compression" && t.raw_value.as_u64() == Some(7)
1241                                }) && tags[before_idx..].iter().any(
1242                                    |t| t.name == "SubfileType" && t.raw_value.as_u64() != Some(0),
1243                                );
1244
1245                                if is_jpeg {
1246                                    // Rename StripOffsets/StripByteCounts based on SubIFD index
1247                                    // Perl: SubIFD2 → JpgFromRaw*, others → PreviewImage*
1248                                    let (start_name, len_name, img_name) = if idx == 2 {
1249                                        ("JpgFromRawStart", "JpgFromRawLength", "JpgFromRaw")
1250                                    } else {
1251                                        ("PreviewImageStart", "PreviewImageLength", "PreviewImage")
1252                                    };
1253                                    // Find StripOffsets and StripByteCounts in this SubIFD
1254                                    let strip_off = tags[before_idx..]
1255                                        .iter()
1256                                        .find(|t| t.name == "StripOffsets")
1257                                        .and_then(|t| t.raw_value.as_u64());
1258                                    let strip_len = tags[before_idx..]
1259                                        .iter()
1260                                        .find(|t| t.name == "StripByteCounts")
1261                                        .and_then(|t| t.raw_value.as_u64());
1262                                    if let (Some(s), Some(l)) = (strip_off, strip_len) {
1263                                        tags.push(Tag {
1264                                            id: TagId::Text(start_name.into()),
1265                                            name: start_name.into(),
1266                                            description: start_name.into(),
1267                                            group: TagGroup {
1268                                                family0: "EXIF".into(),
1269                                                family1: sub_name.clone(),
1270                                                family2: "Preview".into(),
1271                                                family3: "Main".into(),
1272                                            },
1273                                            raw_value: Value::U32(s as u32),
1274                                            print_value: s.to_string(),
1275                                            priority: 0,
1276                                        });
1277                                        tags.push(Tag {
1278                                            id: TagId::Text(len_name.into()),
1279                                            name: len_name.into(),
1280                                            description: len_name.into(),
1281                                            group: TagGroup {
1282                                                family0: "EXIF".into(),
1283                                                family1: sub_name.clone(),
1284                                                family2: "Preview".into(),
1285                                                family3: "Main".into(),
1286                                            },
1287                                            raw_value: Value::U32(l as u32),
1288                                            print_value: l.to_string(),
1289                                            priority: 0,
1290                                        });
1291                                        // Extract binary image data
1292                                        let s = s as usize;
1293                                        let l = l as usize;
1294                                        if l > 0 && s + l <= data.len() {
1295                                            let pv = format!(
1296                                                "(Binary data {} bytes, use -b option to extract)",
1297                                                l
1298                                            );
1299                                            tags.push(Tag {
1300                                                id: TagId::Text(img_name.into()),
1301                                                name: img_name.into(),
1302                                                description: img_name.into(),
1303                                                group: TagGroup {
1304                                                    family0: "EXIF".into(),
1305                                                    family1: sub_name.clone(),
1306                                                    family2: "Preview".into(),
1307                                                    family3: "Main".into(),
1308                                                },
1309                                                raw_value: Value::Binary(data[s..s + l].to_vec()),
1310                                                print_value: pv,
1311                                                priority: 0,
1312                                            });
1313                                        }
1314                                    }
1315                                }
1316
1317                                if is_renamed_offset_pair {
1318                                    let mut i = 0usize;
1319                                    tags.retain(|t| {
1320                                        let keep = i < before_idx
1321                                            || !matches!(
1322                                                t.name.as_str(),
1323                                                "StripOffsets" | "StripByteCounts"
1324                                            );
1325                                        i += 1;
1326                                        keep
1327                                    });
1328                                }
1329
1330                                // Also handle a SubIFD whose 0x0201/0x0202 pair was
1331                                // already named JpgFromRawStart/Length when it was read
1332                                // -- but only if the rename branch above has not already
1333                                // produced the image, or it would be reported twice.
1334                                let jpg_done =
1335                                    tags[before_idx..].iter().any(|t| t.name == "JpgFromRaw");
1336                                let jpg_start = tags[before_idx..]
1337                                    .iter()
1338                                    .find(|t| t.name == "JpgFromRawStart")
1339                                    .and_then(|t| t.raw_value.as_u64());
1340                                let jpg_len = tags[before_idx..]
1341                                    .iter()
1342                                    .find(|t| t.name == "JpgFromRawLength")
1343                                    .and_then(|t| t.raw_value.as_u64());
1344                                if let (false, Some(start), Some(len)) =
1345                                    (jpg_done, jpg_start, jpg_len)
1346                                {
1347                                    let start = start as usize;
1348                                    let len = len as usize;
1349                                    if len > 0 && start + len <= data.len() {
1350                                        let pv = format!(
1351                                            "(Binary data {} bytes, use -b option to extract)",
1352                                            len
1353                                        );
1354                                        tags.push(Tag {
1355                                            id: TagId::Text("JpgFromRaw".into()),
1356                                            name: "JpgFromRaw".into(),
1357                                            description: "Jpg From Raw".into(),
1358                                            group: TagGroup {
1359                                                family0: "EXIF".into(),
1360                                                family1: sub_name,
1361                                                family2: "Preview".into(),
1362                                                family3: "Main".into(),
1363                                            },
1364                                            raw_value: Value::Binary(
1365                                                data[start..start + len].to_vec(),
1366                                            ),
1367                                            print_value: pv,
1368                                            priority: 0,
1369                                        });
1370                                    }
1371                                }
1372                            }
1373                        }
1374                    }
1375                    continue;
1376                }
1377                // CR2 IFD2 (preview JPEG) and IFD3 (raw data) repeat names IFD0
1378                // already defines. ExifTool reads them all and lets the name-keyed
1379                // collapse pick IFD0's ImageWidth/ImageHeight/BitsPerSample/
1380                // Compression and IFD3's StripOffsets/StripByteCounts; with the
1381                // Duplicates option on it reports every copy, so only skip them
1382                // when we are collapsing.
1383                0x0100 | 0x0101 | 0x0102 | 0x0103 | 0x0111 | 0x0117
1384                    if ifd_name == "IFD2" && !keep_duplicates() =>
1385                {
1386                    continue;
1387                }
1388                0x0103 if ifd_name == "IFD3" && !keep_duplicates() => {
1389                    continue;
1390                }
1391                _ => {}
1392            }
1393
1394            if let Some(mut value) = read_ifd_value(data, &entry, header.byte_order) {
1395                // GPS TimeStamp (0x0007): convert 0/0 rationals to 0/1 so it displays as "0, 0, 0"
1396                // (Perl treats 0/0 as 0 for GPS time, enabling GPSDateTime composite)
1397                if ifd_name == "GPS" && entry.tag == 0x0007 {
1398                    if let Value::List(ref mut items) = value {
1399                        for item in items.iter_mut() {
1400                            if matches!(item, Value::URational(0, 0)) {
1401                                *item = Value::URational(0, 1);
1402                            }
1403                        }
1404                    }
1405                }
1406                let tag_info = exif_tags::lookup(ifd_name, entry.tag);
1407                let (name, description, family2) = match tag_info {
1408                    Some(info) => (
1409                        info.name.to_string(),
1410                        info.description.to_string(),
1411                        info.family2.to_string(),
1412                    ),
1413                    None => {
1414                        // Skip known SubDirectory/internal tags that Perl doesn't emit
1415                        if matches!(
1416                            entry.tag,
1417                            // 0x014A handled above (SubIFD traversal)
1418                            // 0x02BC (ApplicationNotes) now parsed as XMP above
1419                            0xC634 // DNG PrivateData — processed after IFD scan
1420                        ) {
1421                            continue;
1422                        }
1423                        // Fallback to generated tags
1424                        match exif_tags::lookup_generated(entry.tag) {
1425                            Some((n, d)) => (n.to_string(), d.to_string(), "Other".to_string()),
1426                            None => {
1427                                // Perl doesn't emit unknown EXIF tags by default
1428                                continue;
1429                            }
1430                        }
1431                    }
1432                };
1433
1434                // Per-tag RawConv that trims trailing blanks. In Exif.pm only a
1435                // handful of string tags do this: Make (0x010f), Model (0x0110),
1436                // Software (0x0131) and Artist (0x013b) each carry
1437                // `RawConv => '$val =~ s/\s+$//'`, and Copyright (0x8298) strips the
1438                // blanks preceding its NUL separator (`s/ *\0/\n/; ...; s/\n$//`),
1439                // which reduces to a trailing-blank trim for the single-part values.
1440                // Every other EXIF string keeps its fixed-width space padding (the
1441                // generic 'string' reader only does `s/\0.*//s`); the padding is
1442                // dropped from text output later by Printable, not from the value.
1443                if matches!(
1444                    name.as_str(),
1445                    "Make" | "Model" | "Software" | "Artist" | "Copyright"
1446                ) {
1447                    if let Value::String(ref s) = value {
1448                        let trimmed = s.trim_end();
1449                        if trimmed.len() != s.len() {
1450                            value = Value::String(trimmed.to_string());
1451                        }
1452                    }
1453                }
1454
1455                // Parse ApplicationNotes (0x02BC) as XMP
1456                if name == "ApplicationNotes" {
1457                    if let Value::Binary(ref xmp_bytes) = value {
1458                        if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_bytes) {
1459                            tags.extend(xmp_tags);
1460                        }
1461                    }
1462                    continue;
1463                }
1464                // Suppress known SubDirectory/internal tags
1465                if matches!(
1466                    name.as_str(),
1467                    "MinSampleValue" | "MaxSampleValue" | // Not emitted by Perl for raw formats
1468                    "ProcessingSoftware" | // Protected tag, not always emitted
1469                    "PanasonicTitle" | "PanasonicTitle2" // DNG tags, wrong match for RW2
1470                ) {
1471                    continue;
1472                }
1473
1474                let print_value = if name.starts_with("Tag0x") && get_show_unknown() >= 2 {
1475                    // -U mode: show binary data for unknown tags
1476                    match &value {
1477                        Value::Binary(bytes) | Value::Undefined(bytes) => bytes
1478                            .iter()
1479                            .map(|b| format!("{:02x}", b))
1480                            .collect::<Vec<_>>()
1481                            .join(" "),
1482                        _ => value.to_display_string(),
1483                    }
1484                } else if name.starts_with("Tag0x") {
1485                    // -u mode: show unknown tags but use standard display for values
1486                    value.to_display_string()
1487                } else {
1488                    exif_tags::print_conv(ifd_name, entry.tag, &value)
1489                        .or_else(|| {
1490                            // Fallback to generated print conversions
1491                            value
1492                                .as_u64()
1493                                .and_then(|v| {
1494                                    crate::tags::print_conv_generated::print_conv_by_name(
1495                                        &name, v as i64,
1496                                    )
1497                                })
1498                                .map(|s| s.to_string())
1499                        })
1500                        .unwrap_or_else(|| value.to_display_string())
1501                };
1502
1503                // Priority the Exif tables state themselves, whether as an
1504                // explicit `Priority => 0` or as the `Avoid => 1` that FoundTag
1505                // resolves to one (ExifTool.pm:9469-9472). Only the IFD tag
1506                // number identifies it: 0xfe54 Contrast is priority 0 while
1507                // 0xa408 Contrast is not, so the name alone would be ambiguous.
1508                let priority =
1509                    if crate::tags::priority0_generated::exif_is_priority0(entry.tag, &name) {
1510                        crate::tag::PRIORITY_EXPLICIT_ZERO
1511                    } else {
1512                        0
1513                    };
1514                tags.push(Tag {
1515                    id: TagId::Numeric(entry.tag),
1516                    name,
1517                    description,
1518                    group: TagGroup {
1519                        family0: "EXIF".to_string(),
1520                        family1: ifd_name.to_string(),
1521                        family2,
1522                        family3: "Main".into(),
1523                    },
1524                    raw_value: value,
1525                    print_value,
1526                    priority,
1527                });
1528            }
1529        }
1530
1531        // Read next IFD offset
1532        let next_ifd_offset = if entries_end + 4 <= data.len() {
1533            read_u32(data, entries_end, header.byte_order)
1534        } else {
1535            0
1536        };
1537        if next_ifd_offset != 0 && ifd_name == "IFD0" {
1538            // IFD1 = thumbnail
1539            let ifd1_start_idx = tags.len();
1540            let ifd1_next = Self::read_ifd(data, header, next_ifd_offset, "IFD1", tags)
1541                .ok()
1542                .flatten();
1543            // IFD1 (the thumbnail IFD) repeats several IFD0 tags — XResolution,
1544            // YResolution, ResolutionUnit, Orientation… Collapsing them here would
1545            // be wrong: ExifTool keeps every instance when the Duplicates option is
1546            // on, which the CLI turns on together with -ee. IFD1 is a
1547            // LOW_PRIORITY_DIR, so the general FoundTag pass in exiftool.rs keeps
1548            // IFD0's copy in the default mode and every copy under -ee. Nothing to
1549            // suppress at parse time.
1550            let _ = ifd1_start_idx;
1551
1552            // Create ThumbnailImage tag if offset+length are present
1553            let thumb_offset = tags
1554                .iter()
1555                .find(|t| t.name == "ThumbnailOffset" && t.group.family1 == "IFD1")
1556                .and_then(|t| t.raw_value.as_u64());
1557            let thumb_length = tags
1558                .iter()
1559                .find(|t| t.name == "ThumbnailLength" && t.group.family1 == "IFD1")
1560                .and_then(|t| t.raw_value.as_u64());
1561
1562            if let (Some(off), Some(len)) = (thumb_offset, thumb_length) {
1563                let off = off as usize;
1564                let len = len as usize;
1565                if off + len <= data.len() && len > 0 {
1566                    tags.push(Tag {
1567                        id: TagId::Text("ThumbnailImage".into()),
1568                        name: "ThumbnailImage".into(),
1569                        description: "Thumbnail Image".into(),
1570                        group: TagGroup {
1571                            family0: "EXIF".into(),
1572                            family1: "IFD1".into(),
1573                            family2: "Image".into(),
1574                            family3: "Main".into(),
1575                        },
1576                        raw_value: Value::Binary(data[off..off + len].to_vec()),
1577                        print_value: format!(
1578                            "(Binary data {} bytes, use -b option to extract)",
1579                            len
1580                        ),
1581                        priority: 0,
1582                    });
1583                }
1584            }
1585
1586            // CR2 files have additional IFDs (IFD2, IFD3) following IFD1 in the chain.
1587            // CR2 is identified by "CR" bytes at offset 8 in the TIFF data.
1588            let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
1589            if is_cr2 {
1590                if let Some(ifd2_offset) = ifd1_next {
1591                    // IFD2 = preview JPEG image data (emit selected tags)
1592                    let ifd2_next = Self::read_ifd(data, header, ifd2_offset, "IFD2", tags)
1593                        .ok()
1594                        .flatten();
1595                    // IFD3 = raw image data (emit CR2CFAPattern, RawImageSegmentation, StripOffsets, StripByteCounts)
1596                    if let Some(ifd3_offset) = ifd2_next {
1597                        let _ = Self::read_ifd(data, header, ifd3_offset, "IFD3", tags);
1598                    }
1599                }
1600            }
1601        }
1602
1603        Ok(if next_ifd_offset != 0 {
1604            Some(next_ifd_offset)
1605        } else {
1606            None
1607        })
1608    }
1609
1610    /// Parse a TIFF where IFD0 is treated as a named IFD (e.g. "GPS", "ExifIFD").
1611    /// Used for CR3 CMT4 (GPS-only TIFF) and CMT2 (ExifIFD-only TIFF).
1612    /// Does no MakerNote/IFD1 processing.
1613    pub fn read_as_named_ifd(data: &[u8], ifd_name: &str) -> Vec<Tag> {
1614        let header = match parse_tiff_header(data) {
1615            Ok(h) => h,
1616            Err(_) => return Vec::new(),
1617        };
1618        let mut tags = Vec::new();
1619        // Each of these boxes is a TIFF file of its own, and ExifTool's
1620        // ProcessTIFF raises ExifByteOrder once per TIFF it processes — a CR3
1621        // therefore reports it for CMT1, CMT2 and CMT4 (verified with -v2 on
1622        // CanonRaw.cr3; CMT3 goes through the maker-note path and raises none).
1623        tags.push(Self::exif_byte_order_tag(header.byte_order));
1624        let _ = Self::read_ifd(data, &header, header.ifd0_offset, ifd_name, &mut tags);
1625        tags
1626    }
1627
1628    /// Parse a single IFD located at `offset` inside a TIFF-like container whose
1629    /// header this reader cannot parse itself (RW2 uses magic 0x55 instead of
1630    /// 0x2A). Offsets inside the IFD stay relative to the start of `data`, which
1631    /// is what a TIFF IFD always uses. No ExifByteOrder and no IFD1 chaining.
1632    pub fn read_ifd_at(data: &[u8], little_endian: bool, offset: u32, ifd_name: &str) -> Vec<Tag> {
1633        let header = TiffHeader {
1634            byte_order: if little_endian {
1635                ByteOrderMark::LittleEndian
1636            } else {
1637                ByteOrderMark::BigEndian
1638            },
1639            ifd0_offset: offset,
1640        };
1641        let mut tags = Vec::new();
1642        let _ = Self::read_ifd(data, &header, offset, ifd_name, &mut tags);
1643        tags
1644    }
1645}
1646
1647fn parse_ifd_entry(data: &[u8], offset: usize, byte_order: ByteOrderMark) -> IfdEntry {
1648    let tag = read_u16(data, offset, byte_order);
1649    let data_type = read_u16(data, offset + 2, byte_order);
1650    let count = read_u32(data, offset + 4, byte_order);
1651    let value_offset = read_u32(data, offset + 8, byte_order);
1652    let mut inline_data = [0u8; 4];
1653    inline_data.copy_from_slice(&data[offset + 8..offset + 12]);
1654
1655    IfdEntry {
1656        tag,
1657        data_type,
1658        count,
1659        value_offset,
1660        inline_data,
1661    }
1662}
1663
1664fn read_ifd_value(data: &[u8], entry: &IfdEntry, byte_order: ByteOrderMark) -> Option<Value> {
1665    let elem_size = type_size(entry.data_type)?;
1666    let total_size = elem_size * entry.count as usize;
1667
1668    let value_data = if total_size <= 4 {
1669        &entry.inline_data[..total_size]
1670    } else {
1671        let offset = entry.value_offset as usize;
1672        if offset + total_size > data.len() {
1673            return None;
1674        }
1675        &data[offset..offset + total_size]
1676    };
1677
1678    // IPTC-NAA (0x83BB): always read as raw binary regardless of declared type
1679    if entry.tag == 0x83BB {
1680        return Some(Value::Binary(value_data.to_vec()));
1681    }
1682
1683    // ApplicationNotes (0x02BC): always read as raw binary (XMP data)
1684    if entry.tag == 0x02BC {
1685        return Some(Value::Binary(value_data.to_vec()));
1686    }
1687
1688    match entry.data_type {
1689        // BYTE
1690        1 => {
1691            if entry.count == 1 {
1692                Some(Value::U8(value_data[0]))
1693            } else {
1694                Some(Value::List(
1695                    value_data.iter().map(|&b| Value::U8(b)).collect(),
1696                ))
1697            }
1698        }
1699        // ASCII
1700        2 => {
1701            let s = crate::encoding::decode_utf8_or_latin1(value_data);
1702            // ExifTool truncates at the first null only (ExifTool.pm:10038
1703            // `$val =~ s/\0.*//s`). Trailing blanks that pad a fixed-width field
1704            // (e.g. "OLYMPUS DIGITAL CAMERA         ") are PRESERVED in the stored
1705            // value; they are stripped only at text-output time by Printable
1706            // (exiftool:3009 `$val =~ s/\s+$//`), which our text path mirrors in
1707            // sanitize_display_value. JSON output keeps them, matching ExifTool.
1708            let s = s.split('\0').next().unwrap_or("").to_string();
1709            Some(Value::String(s))
1710        }
1711        // SHORT
1712        3 => {
1713            if entry.count == 1 {
1714                Some(Value::U16(read_u16(value_data, 0, byte_order)))
1715            } else {
1716                let vals: Vec<Value> = (0..entry.count as usize)
1717                    .map(|i| Value::U16(read_u16(value_data, i * 2, byte_order)))
1718                    .collect();
1719                Some(Value::List(vals))
1720            }
1721        }
1722        // LONG
1723        4 | 13 => {
1724            if entry.count == 1 {
1725                Some(Value::U32(read_u32(value_data, 0, byte_order)))
1726            } else {
1727                let vals: Vec<Value> = (0..entry.count as usize)
1728                    .map(|i| Value::U32(read_u32(value_data, i * 4, byte_order)))
1729                    .collect();
1730                Some(Value::List(vals))
1731            }
1732        }
1733        // RATIONAL (unsigned)
1734        5 => {
1735            if entry.count == 1 {
1736                let n = read_u32(value_data, 0, byte_order);
1737                let d = read_u32(value_data, 4, byte_order);
1738                Some(Value::URational(n, d))
1739            } else {
1740                let vals: Vec<Value> = (0..entry.count as usize)
1741                    .map(|i| {
1742                        let n = read_u32(value_data, i * 8, byte_order);
1743                        let d = read_u32(value_data, i * 8 + 4, byte_order);
1744                        Value::URational(n, d)
1745                    })
1746                    .collect();
1747                Some(Value::List(vals))
1748            }
1749        }
1750        // SBYTE
1751        6 => {
1752            if entry.count == 1 {
1753                Some(Value::I16(value_data[0] as i8 as i16))
1754            } else {
1755                let vals: Vec<Value> = value_data
1756                    .iter()
1757                    .map(|&b| Value::I16(b as i8 as i16))
1758                    .collect();
1759                Some(Value::List(vals))
1760            }
1761        }
1762        // UNDEFINED
1763        7 => Some(Value::Undefined(value_data.to_vec())),
1764        // SSHORT
1765        8 => {
1766            if entry.count == 1 {
1767                Some(Value::I16(read_i16(value_data, 0, byte_order)))
1768            } else {
1769                let vals: Vec<Value> = (0..entry.count as usize)
1770                    .map(|i| Value::I16(read_i16(value_data, i * 2, byte_order)))
1771                    .collect();
1772                Some(Value::List(vals))
1773            }
1774        }
1775        // SLONG
1776        9 => {
1777            if entry.count == 1 {
1778                Some(Value::I32(read_i32(value_data, 0, byte_order)))
1779            } else {
1780                let vals: Vec<Value> = (0..entry.count as usize)
1781                    .map(|i| Value::I32(read_i32(value_data, i * 4, byte_order)))
1782                    .collect();
1783                Some(Value::List(vals))
1784            }
1785        }
1786        // SRATIONAL
1787        10 => {
1788            if entry.count == 1 {
1789                let n = read_i32(value_data, 0, byte_order);
1790                let d = read_i32(value_data, 4, byte_order);
1791                Some(Value::IRational(n, d))
1792            } else {
1793                let vals: Vec<Value> = (0..entry.count as usize)
1794                    .map(|i| {
1795                        let n = read_i32(value_data, i * 8, byte_order);
1796                        let d = read_i32(value_data, i * 8 + 4, byte_order);
1797                        Value::IRational(n, d)
1798                    })
1799                    .collect();
1800                Some(Value::List(vals))
1801            }
1802        }
1803        // FLOAT
1804        11 => {
1805            if entry.count == 1 {
1806                let bits = read_u32(value_data, 0, byte_order);
1807                Some(Value::F32(f32::from_bits(bits)))
1808            } else {
1809                let vals: Vec<Value> = (0..entry.count as usize)
1810                    .map(|i| {
1811                        let bits = read_u32(value_data, i * 4, byte_order);
1812                        Value::F32(f32::from_bits(bits))
1813                    })
1814                    .collect();
1815                Some(Value::List(vals))
1816            }
1817        }
1818        // DOUBLE
1819        12 => {
1820            if entry.count == 1 {
1821                let bits = read_u64(value_data, 0, byte_order);
1822                Some(Value::F64(f64::from_bits(bits)))
1823            } else {
1824                let vals: Vec<Value> = (0..entry.count as usize)
1825                    .map(|i| {
1826                        let bits = read_u64(value_data, i * 8, byte_order);
1827                        Value::F64(f64::from_bits(bits))
1828                    })
1829                    .collect();
1830                Some(Value::List(vals))
1831            }
1832        }
1833        _ => None,
1834    }
1835}
1836
1837// Byte-order-aware read helpers
1838fn read_u16(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1839    match bo {
1840        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[offset..]),
1841        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[offset..]),
1842    }
1843}
1844
1845fn read_u32(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1846    match bo {
1847        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[offset..]),
1848        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[offset..]),
1849    }
1850}
1851
1852fn read_u64(data: &[u8], offset: usize, bo: ByteOrderMark) -> u64 {
1853    match bo {
1854        ByteOrderMark::LittleEndian => LittleEndian::read_u64(&data[offset..]),
1855        ByteOrderMark::BigEndian => BigEndian::read_u64(&data[offset..]),
1856    }
1857}
1858
1859fn read_i16(data: &[u8], offset: usize, bo: ByteOrderMark) -> i16 {
1860    match bo {
1861        ByteOrderMark::LittleEndian => LittleEndian::read_i16(&data[offset..]),
1862        ByteOrderMark::BigEndian => BigEndian::read_i16(&data[offset..]),
1863    }
1864}
1865
1866fn read_i32(data: &[u8], offset: usize, bo: ByteOrderMark) -> i32 {
1867    match bo {
1868        ByteOrderMark::LittleEndian => LittleEndian::read_i32(&data[offset..]),
1869        ByteOrderMark::BigEndian => BigEndian::read_i32(&data[offset..]),
1870    }
1871}
1872
1873/// Process GeoTIFF key directory (tag GeoTiffDirectory / GeoKeyDirectory)
1874/// and replace raw directory/ascii/double params with named GeoTIFF tags.
1875fn process_geotiff_keys(tags: &mut Vec<Tag>) {
1876    // Extract GeoTiffDirectory values
1877    let dir_vals: Option<Vec<u16>> =
1878        tags.iter()
1879            .find(|t| t.name == "GeoTiffDirectory")
1880            .and_then(|t| match &t.raw_value {
1881                Value::List(items) => {
1882                    let vals: Vec<u16> = items
1883                        .iter()
1884                        .filter_map(|v| match v {
1885                            Value::U16(x) => Some(*x),
1886                            Value::U32(x) => Some(*x as u16),
1887                            _ => None,
1888                        })
1889                        .collect();
1890                    if vals.is_empty() {
1891                        None
1892                    } else {
1893                        Some(vals)
1894                    }
1895                }
1896                _ => None,
1897            });
1898
1899    let dir_vals = match dir_vals {
1900        Some(v) => v,
1901        None => return,
1902    };
1903
1904    if dir_vals.len() < 4 {
1905        return;
1906    }
1907
1908    let version = dir_vals[0];
1909    let revision = dir_vals[1];
1910    let minor_rev = dir_vals[2];
1911    let num_entries = dir_vals[3] as usize;
1912
1913    if dir_vals.len() < 4 + num_entries * 4 {
1914        return;
1915    }
1916
1917    // Extract ASCII params
1918    let ascii_params: Option<String> = tags
1919        .iter()
1920        .find(|t| t.name == "GeoTiffAsciiParams")
1921        .map(|t| t.print_value.clone());
1922
1923    // Extract double params
1924    let double_params: Option<Vec<f64>> = tags
1925        .iter()
1926        .find(|t| t.name == "GeoTiffDoubleParams")
1927        .and_then(|t| match &t.raw_value {
1928            Value::List(items) => {
1929                let vals: Vec<f64> = items
1930                    .iter()
1931                    .filter_map(|v| match v {
1932                        Value::F64(x) => Some(*x),
1933                        Value::F32(x) => Some(*x as f64),
1934                        _ => None,
1935                    })
1936                    .collect();
1937                if vals.is_empty() {
1938                    None
1939                } else {
1940                    Some(vals)
1941                }
1942            }
1943            _ => None,
1944        });
1945
1946    let mut new_tags = Vec::new();
1947
1948    // Version tag
1949    new_tags.push(Tag {
1950        id: TagId::Text("GeoTiffVersion".to_string()),
1951        name: "GeoTiffVersion".to_string(),
1952        description: "GeoTiff Version".to_string(),
1953        group: TagGroup {
1954            // GeoTiff.pm's own group, not the IFD0 tag that carries the keys.
1955            family0: "GeoTiff".into(),
1956            family1: "GeoTiff".into(),
1957            family2: "Location".into(),
1958            family3: "Main".into(),
1959        },
1960        raw_value: Value::String(format!("{}.{}.{}", version, revision, minor_rev)),
1961        print_value: format!("{}.{}.{}", version, revision, minor_rev),
1962        priority: 0,
1963    });
1964
1965    // Process each GeoKey
1966    for i in 0..num_entries {
1967        let base = 4 + i * 4;
1968        let key_id = dir_vals[base];
1969        let location = dir_vals[base + 1];
1970        let count = dir_vals[base + 2] as usize;
1971        let value_or_offset = dir_vals[base + 3];
1972
1973        let raw_val: Option<String> = match location {
1974            0 => {
1975                // Value stored inline in value_or_offset
1976                Some(format!("{}", value_or_offset))
1977            }
1978            34737 => {
1979                // ASCII params
1980                if let Some(ref ascii) = ascii_params {
1981                    let off = value_or_offset as usize;
1982                    let end = (off + count).min(ascii.len());
1983                    if off <= end {
1984                        let s = &ascii[off..end];
1985                        // Remove trailing '|' separators
1986                        let s = s.trim_end_matches('|').trim().to_string();
1987                        Some(s)
1988                    } else {
1989                        None
1990                    }
1991                } else {
1992                    None
1993                }
1994            }
1995            34736 => {
1996                // Double params
1997                if let Some(ref doubles) = double_params {
1998                    let off = value_or_offset as usize;
1999                    if count == 1 && off < doubles.len() {
2000                        Some(format!("{}", doubles[off]))
2001                    } else if count > 1 {
2002                        let vals: Vec<String> = doubles
2003                            .iter()
2004                            .skip(off)
2005                            .take(count)
2006                            .map(|v| format!("{}", v))
2007                            .collect();
2008                        Some(vals.join(" "))
2009                    } else {
2010                        None
2011                    }
2012                } else {
2013                    None
2014                }
2015            }
2016            _ => None,
2017        };
2018
2019        let val_str = match raw_val {
2020            Some(v) => v,
2021            None => continue,
2022        };
2023
2024        // Map GeoKey ID to tag name and print value
2025        let (tag_name, print_val) = geotiff_key_to_tag(key_id, &val_str);
2026        if tag_name.is_empty() {
2027            continue;
2028        }
2029
2030        new_tags.push(Tag {
2031            id: TagId::Text(tag_name.clone()),
2032            name: tag_name.clone(),
2033            description: tag_name.clone(),
2034            group: TagGroup {
2035                // GeoTIFF keys travel inside an IFD0 tag, but ExifTool reads
2036                // them with GeoTiff.pm, a group of its own -- same as the
2037                // GeoTiffVersion tag emitted just above.
2038                family0: "GeoTiff".into(),
2039                family1: "GeoTiff".into(),
2040                family2: "Location".into(),
2041                family3: "Main".into(),
2042            },
2043            raw_value: Value::String(val_str),
2044            print_value: print_val,
2045            priority: 0,
2046        });
2047    }
2048
2049    if !new_tags.is_empty() {
2050        // Remove raw GeoTIFF tags
2051        tags.retain(|t| {
2052            t.name != "GeoTiffDirectory"
2053                && t.name != "GeoTiffAsciiParams"
2054                && t.name != "GeoTiffDoubleParams"
2055        });
2056        tags.extend(new_tags);
2057    }
2058}
2059
2060/// Map a GeoKey ID to (tag_name, print_value).
2061fn geotiff_key_to_tag(key_id: u16, value: &str) -> (String, String) {
2062    let val_u16: Option<u16> = value.parse().ok();
2063
2064    match key_id {
2065        // Section 6.2.1: GeoTIFF Configuration Keys
2066        0x0001 => return ("GeoTiffVersion".to_string(), value.to_string()), // not used here
2067        0x0400 => {
2068            // GTModelType
2069            let print = match val_u16 {
2070                Some(1) => "Projected".to_string(),
2071                Some(2) => "Geographic".to_string(),
2072                Some(3) => "Geocentric".to_string(),
2073                Some(32767) => "User Defined".to_string(),
2074                _ => value.to_string(),
2075            };
2076            return ("GTModelType".to_string(), print);
2077        }
2078        0x0401 => {
2079            // GTRasterType
2080            let print = match val_u16 {
2081                Some(1) => "Pixel Is Area".to_string(),
2082                Some(2) => "Pixel Is Point".to_string(),
2083                Some(32767) => "User Defined".to_string(),
2084                _ => value.to_string(),
2085            };
2086            return ("GTRasterType".to_string(), print);
2087        }
2088        0x0402 => return ("GTCitation".to_string(), value.to_string()),
2089
2090        // Section 6.2.2: Geographic CS Parameter Keys
2091        0x0800 => {
2092            return (
2093                "GeographicType".to_string(),
2094                geotiff_pcs_name(val_u16.unwrap_or(0), value),
2095            )
2096        }
2097        0x0801 => return ("GeogCitation".to_string(), value.to_string()),
2098        0x0802 => {
2099            let print = match val_u16 {
2100                Some(32767) | Some(32766) => "User Defined".to_string(),
2101                _ => value.to_string(),
2102            };
2103            return ("GeogGeodeticDatum".to_string(), print);
2104        }
2105        0x0803 => return ("GeogPrimeMeridian".to_string(), value.to_string()),
2106        0x0804 => {
2107            return (
2108                "GeogLinearUnits".to_string(),
2109                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
2110            )
2111        }
2112        0x0805 => return ("GeogLinearUnitSize".to_string(), value.to_string()),
2113        0x0806 => return ("GeogAngularUnits".to_string(), value.to_string()),
2114        0x0807 => return ("GeogAngularUnitSize".to_string(), value.to_string()),
2115        0x0808 => return ("GeogEllipsoid".to_string(), value.to_string()),
2116        0x0809 => return ("GeogSemiMajorAxis".to_string(), value.to_string()),
2117        0x080a => return ("GeogSemiMinorAxis".to_string(), value.to_string()),
2118        0x080b => return ("GeogInvFlattening".to_string(), value.to_string()),
2119        0x080c => return ("GeogAzimuthUnits".to_string(), value.to_string()),
2120        0x080d => return ("GeogPrimeMeridianLong".to_string(), value.to_string()),
2121
2122        // Section 6.2.3: Projected CS Parameter Keys
2123        0x0C00 => {
2124            // ProjectedCSType
2125            return (
2126                "ProjectedCSType".to_string(),
2127                geotiff_pcs_name(val_u16.unwrap_or(0), value),
2128            );
2129        }
2130        0x0C01 => return ("PCSCitation".to_string(), value.to_string()),
2131        0x0C02 => {
2132            // UTM zones follow a regular range in GeoTiff.pm's Projection table:
2133            // 16001-16060 = UTM zone N (north), 16101-16160 = UTM zone N (south).
2134            let print = match val_u16 {
2135                Some(v @ 16001..=16060) => format!("UTM zone {}N", v - 16000),
2136                Some(v @ 16101..=16160) => format!("UTM zone {}S", v - 16100),
2137                Some(32767) | Some(32766) => "User Defined".to_string(),
2138                _ => value.to_string(),
2139            };
2140            return ("Projection".to_string(), print);
2141        }
2142        0x0C03 => return ("ProjCoordTrans".to_string(), value.to_string()),
2143        0x0C04 => {
2144            return (
2145                "ProjLinearUnits".to_string(),
2146                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
2147            )
2148        }
2149        0x0C05 => return ("ProjLinearUnitSize".to_string(), value.to_string()),
2150        0x0C06 => return ("ProjStdParallel1".to_string(), value.to_string()),
2151        0x0C07 => return ("ProjStdParallel2".to_string(), value.to_string()),
2152        0x0C08 => return ("ProjNatOriginLong".to_string(), value.to_string()),
2153        0x0C09 => return ("ProjNatOriginLat".to_string(), value.to_string()),
2154        0x0c0a => return ("ProjFalseEasting".to_string(), value.to_string()),
2155        0x0c0b => return ("ProjFalseNorthing".to_string(), value.to_string()),
2156        0x0c0c => return ("ProjFalseOriginLong".to_string(), value.to_string()),
2157        0x0c0d => return ("ProjFalseOriginLat".to_string(), value.to_string()),
2158        0x0c0e => return ("ProjFalseOriginEasting".to_string(), value.to_string()),
2159        0x0c0f => return ("ProjFalseOriginNorthing".to_string(), value.to_string()),
2160        0x0C10 => return ("ProjCenterLong".to_string(), value.to_string()),
2161        0x0C11 => return ("ProjCenterLat".to_string(), value.to_string()),
2162        0x0C12 => return ("ProjCenterEasting".to_string(), value.to_string()),
2163        0x0C13 => return ("ProjCenterNorthing".to_string(), value.to_string()),
2164        0x0C14 => return ("ProjScaleAtNatOrigin".to_string(), value.to_string()),
2165        0x0C15 => return ("ProjScaleAtCenter".to_string(), value.to_string()),
2166        0x0C16 => return ("ProjAzimuthAngle".to_string(), value.to_string()),
2167        0x0C17 => return ("ProjStraightVertPoleLong".to_string(), value.to_string()),
2168
2169        // Section 6.2.4: Vertical CS Keys
2170        0x1000 => return ("VerticalCSType".to_string(), value.to_string()),
2171        0x1001 => return ("VerticalCitation".to_string(), value.to_string()),
2172        0x1002 => return ("VerticalDatum".to_string(), value.to_string()),
2173        0x1003 => {
2174            return (
2175                "VerticalUnits".to_string(),
2176                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
2177            )
2178        }
2179
2180        _ => {}
2181    }
2182    (String::new(), String::new())
2183}
2184
2185fn geotiff_linear_unit_name(val: u16, fallback: &str) -> String {
2186    match val {
2187        9001 => "Linear Meter".to_string(),
2188        9002 => "Linear Foot".to_string(),
2189        9003 => "Linear Foot US Survey".to_string(),
2190        9004 => "Linear Foot Modified American".to_string(),
2191        9005 => "Linear Foot Clarke".to_string(),
2192        9006 => "Linear Foot Indian".to_string(),
2193        9007 => "Linear Link".to_string(),
2194        9008 => "Linear Link Benoit".to_string(),
2195        9009 => "Linear Link Sears".to_string(),
2196        9010 => "Linear Chain Benoit".to_string(),
2197        9011 => "Linear Chain Sears".to_string(),
2198        9012 => "Linear Yard Sears".to_string(),
2199        9013 => "Linear Yard Indian".to_string(),
2200        9014 => "Linear Fathom".to_string(),
2201        9015 => "Linear Mile International Nautical".to_string(),
2202        _ => fallback.to_string(),
2203    }
2204}
2205
2206fn geotiff_pcs_name(val: u16, fallback: &str) -> String {
2207    // Common PCS codes - just return the code with description for common ones
2208    match val {
2209        26918 => "NAD83 UTM zone 18N".to_string(),
2210        26919 => "NAD83 UTM zone 19N".to_string(),
2211        32618 => "WGS84 UTM zone 18N".to_string(),
2212        32619 => "WGS84 UTM zone 19N".to_string(),
2213        4326 => "WGS 84".to_string(),
2214        4269 => "NAD83".to_string(),
2215        4267 => "NAD27".to_string(),
2216        32767 => "User Defined".to_string(),
2217        _ => fallback.to_string(),
2218    }
2219}