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;
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}
18
19/// Set the show_unknown level for the current thread (used by MakerNotes).
20pub fn set_show_unknown(level: u8) {
21    SHOW_UNKNOWN.with(|s| s.set(level));
22}
23
24/// Get the show_unknown level for the current thread (used by MakerNotes).
25pub fn get_show_unknown() -> u8 {
26    SHOW_UNKNOWN.with(|s| s.get())
27}
28
29/// Byte order of the TIFF data.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ByteOrderMark {
32    LittleEndian,
33    BigEndian,
34}
35
36/// Parsed TIFF header.
37#[derive(Debug)]
38pub struct TiffHeader {
39    pub byte_order: ByteOrderMark,
40    pub ifd0_offset: u32,
41}
42
43/// EXIF IFD entry as read from the file.
44#[derive(Debug)]
45struct IfdEntry {
46    tag: u16,
47    data_type: u16,
48    count: u32,
49    value_offset: u32,
50    /// For values that fit in 4 bytes, the raw 4 bytes
51    inline_data: [u8; 4],
52}
53
54/// Size in bytes for each TIFF data type.
55fn type_size(data_type: u16) -> Option<usize> {
56    match data_type {
57        1 => Some(1),  // BYTE
58        2 => Some(1),  // ASCII
59        3 => Some(2),  // SHORT
60        4 => Some(4),  // LONG
61        5 => Some(8),  // RATIONAL
62        6 => Some(1),  // SBYTE
63        7 => Some(1),  // UNDEFINED
64        8 => Some(2),  // SSHORT
65        9 => Some(4),  // SLONG
66        10 => Some(8), // SRATIONAL
67        11 => Some(4), // FLOAT
68        12 => Some(8), // DOUBLE
69        13 => Some(4), // IFD
70        _ => None,
71    }
72}
73
74/// Parse a TIFF header from raw bytes.
75pub fn parse_tiff_header(data: &[u8]) -> Result<TiffHeader> {
76    if data.len() < 8 {
77        return Err(Error::InvalidTiffHeader);
78    }
79
80    let byte_order = match (data[0], data[1]) {
81        (b'I', b'I') => ByteOrderMark::LittleEndian,
82        (b'M', b'M') => ByteOrderMark::BigEndian,
83        _ => return Err(Error::InvalidTiffHeader),
84    };
85
86    let magic = match byte_order {
87        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[2..4]),
88        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[2..4]),
89    };
90
91    if magic != 42 {
92        return Err(Error::InvalidTiffHeader);
93    }
94
95    let ifd0_offset = match byte_order {
96        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[4..8]),
97        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[4..8]),
98    };
99
100    Ok(TiffHeader {
101        byte_order,
102        ifd0_offset,
103    })
104}
105
106/// Tags where the EXIF IFD value takes priority over a same-named MakerNotes tag
107/// (structural/authoritative EXIF). For all other duplicates, MakerNotes wins —
108/// matching ExifTool's group priority.
109pub(crate) const EXIF_PRIMARY_TAGS: &[&str] = &[
110    "ThumbnailOffset",
111    "ThumbnailLength",
112    "ThumbnailImage",
113    "StripOffsets",
114    "StripByteCounts",
115    "PreviewImageStart",
116    "PreviewImageLength",
117    "PreviewImage",
118    "ImageWidth",
119    "ImageHeight",
120    "BitsPerSample",
121    "Compression",
122    "PhotometricInterpretation",
123    "SamplesPerPixel",
124    "RowsPerStrip",
125    "PlanarConfiguration",
126    "XResolution",
127    "YResolution",
128    "ResolutionUnit",
129    "Orientation",
130    "Make",
131    "Model",
132    "Software",
133    "ExifByteOrder",
134    "CR2CFAPattern",
135    "RawImageSegmentation",
136    "ColorSpace",
137    "ExifVersion",
138    "FlashpixVersion",
139    "ExifImageWidth",
140    "ExifImageHeight",
141    "InteropIndex",
142    "InteropVersion",
143    "DateTimeOriginal",
144    "CreateDate",
145    "ModifyDate",
146    "DateTime",
147    "FocalPlaneXResolution",
148    "FocalPlaneYResolution",
149    "FocalPlaneResolutionUnit",
150    "CustomRendered",
151    "ExposureMode",
152    "SceneCaptureType",
153    // IFD0 PrintIM wins over a MakerNotes PrintIM copy.
154    "PrintIMVersion",
155    "Flash",
156    "FocalLength",
157    "ISO",
158    "ExposureTime",
159    "ExposureProgram",
160    "FNumber",
161    "ShutterSpeedValue",
162    "ApertureValue",
163    "ComponentsConfiguration",
164    "UserComment",
165    // Standard EXIF image-parameter tags (0xa408-0xa40c) take priority over the
166    // manufacturer's MakerNote duplicate when both are present (ExifTool default).
167    "Contrast",
168    "Saturation",
169    "Sharpness",
170];
171
172/// EXIF metadata reader.
173pub struct ExifReader;
174
175impl ExifReader {
176    /// Parse EXIF data from a byte slice (starting at the TIFF header).
177    pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
178        Self::read_with_base(data, 0)
179    }
180
181    /// Parse EXIF data, adding `base` (the TIFF header's offset within the file) to
182    /// offset-type tags so they read as absolute file offsets, matching ExifTool.
183    pub fn read_with_base(data: &[u8], base: usize) -> Result<Vec<Tag>> {
184        let mut tags = Self::read_inner(data, base)?;
185        if base != 0 {
186            // ExifTool reports these IsOffset tags relative to the start of the file.
187            const OFFSET_TAGS: &[&str] = &[
188                "ThumbnailOffset",
189                "PreviewImageStart",
190                "JpgFromRawStart",
191                "OtherImageStart",
192                "StripOffsets",
193            ];
194            for t in tags.iter_mut() {
195                if OFFSET_TAGS.contains(&t.name.as_str()) {
196                    if let Some(off) = t.raw_value.as_u64() {
197                        let abs = off + base as u64;
198                        t.raw_value = Value::U32(abs as u32);
199                        t.print_value = abs.to_string();
200                    }
201                }
202            }
203        }
204        Ok(tags)
205    }
206
207    fn read_inner(data: &[u8], exif_base: usize) -> Result<Vec<Tag>> {
208        let header = parse_tiff_header(data)?;
209        let mut tags = Vec::new();
210
211        // Emit ExifByteOrder tag
212        let bo_str = match header.byte_order {
213            ByteOrderMark::LittleEndian => "Little-endian (Intel, II)",
214            ByteOrderMark::BigEndian => "Big-endian (Motorola, MM)",
215        };
216        tags.push(Tag {
217            id: TagId::Text("ExifByteOrder".to_string()),
218            name: "ExifByteOrder".to_string(),
219            description: "Exif Byte Order".to_string(),
220            group: TagGroup {
221                family0: "EXIF".to_string(),
222                family1: "IFD0".to_string(),
223                family2: "ExifTool".to_string(),
224            },
225            raw_value: Value::String(bo_str.to_string()),
226            print_value: bo_str.to_string(),
227            priority: 0,
228        });
229
230        // Detect CR2: "CR" at offset 8 in TIFF data
231        let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
232
233        // Read IFD0 (main image)
234        Self::read_ifd(data, &header, header.ifd0_offset, "IFD0", &mut tags)?;
235
236        // For CR2 files, rename IFD0 StripOffsets→PreviewImageStart and
237        // StripByteCounts→PreviewImageLength, then construct PreviewImage.
238        if is_cr2 {
239            // Rename tags in-place
240            for tag in tags.iter_mut() {
241                if tag.group.family1 == "IFD0" {
242                    if tag.name == "StripOffsets" {
243                        tag.name = "PreviewImageStart".to_string();
244                        tag.description = "Preview Image Start".to_string();
245                        tag.id = TagId::Text("PreviewImageStart".to_string());
246                    } else if tag.name == "StripByteCounts" {
247                        tag.name = "PreviewImageLength".to_string();
248                        tag.description = "Preview Image Length".to_string();
249                        tag.id = TagId::Text("PreviewImageLength".to_string());
250                    }
251                }
252            }
253            // Construct PreviewImage from PreviewImageStart + PreviewImageLength
254            let preview_start = tags
255                .iter()
256                .find(|t| t.name == "PreviewImageStart" && t.group.family1 == "IFD0")
257                .and_then(|t| t.raw_value.as_u64())
258                .map(|v| v as usize);
259            let preview_len = tags
260                .iter()
261                .find(|t| t.name == "PreviewImageLength" && t.group.family1 == "IFD0")
262                .and_then(|t| t.raw_value.as_u64())
263                .map(|v| v as usize);
264            if let (Some(start), Some(len)) = (preview_start, preview_len) {
265                if len > 0 && start + len <= data.len() {
266                    let img_data = data[start..start + len].to_vec();
267                    let pv = format!("(Binary data {} bytes, use -b option to extract)", len);
268                    tags.push(Tag {
269                        id: TagId::Text("PreviewImage".to_string()),
270                        name: "PreviewImage".to_string(),
271                        description: "Preview Image".to_string(),
272                        group: TagGroup {
273                            family0: "EXIF".to_string(),
274                            family1: "IFD0".to_string(),
275                            family2: "Preview".to_string(),
276                        },
277                        raw_value: Value::Binary(img_data),
278                        print_value: pv,
279                        priority: 0,
280                    });
281                }
282            }
283        }
284
285        // Extract Make + Model for MakerNotes detection and sub-table dispatch
286        let make = tags
287            .iter()
288            .find(|t| t.name == "Make")
289            .map(|t| t.print_value.clone())
290            .unwrap_or_default();
291
292        let model = tags
293            .iter()
294            .find(|t| t.name == "Model")
295            .map(|t| t.print_value.clone())
296            .unwrap_or_default();
297
298        // Store model for sub-table dispatch
299        let make_and_model = if model.is_empty() {
300            make.clone()
301        } else {
302            model
303        };
304
305        // Find and parse MakerNotes
306        // Look for the MakerNote tag (0x927C) that was stored as Undefined
307        let mn_info: Option<(usize, usize)> = {
308            // Re-scan ExifIFD for MakerNote offset/size
309            let mut result = None;
310            Self::find_makernote(data, &header, &mut result);
311            result
312        };
313
314        if let Some((mn_offset, mn_size)) = mn_info {
315            let mn_tags = crate::metadata::makernotes::parse_makernotes_exif_base(
316                data,
317                mn_offset,
318                mn_size,
319                &make,
320                &make_and_model,
321                header.byte_order,
322                exif_base,
323            );
324            // Remove the raw MakerNote tag and replace with parsed tags
325            tags.retain(|t| t.name != "MakerNote");
326            // In Perl ExifTool, MakerNotes tags with equal/higher priority overwrite EXIF tags.
327            // Tags in the EXIF-primary list: EXIF wins (skip MakerNotes duplicate).
328            // Other tags: MakerNotes wins (remove EXIF version, add MakerNotes version).
329            {
330                // Tags where EXIF takes priority over MakerNotes (structural/authoritative EXIF)
331                let exif_primary: &[&str] = EXIF_PRIMARY_TAGS;
332                // Only maker-note tags with non-negative priority remove the EXIF
333                // duplicate. Tags from a PRIORITY=>0 sub-block demoted to -1 (e.g.
334                // Minolta CameraSettings) must not override a standard EXIF tag.
335                let mn_name_set: std::collections::HashSet<String> = mn_tags
336                    .iter()
337                    .filter(|t| t.priority >= 0)
338                    .map(|t| t.name.clone())
339                    .collect();
340                let exif_has: std::collections::HashSet<String> =
341                    tags.iter().map(|t| t.name.clone()).collect();
342                // Remove EXIF non-primary tags when MakerNotes provides them (MakerNotes wins)
343                tags.retain(|t| {
344                    !mn_name_set.contains(&t.name) || exif_primary.contains(&t.name.as_str())
345                });
346                // Add MakerNotes tags, but skip EXIF-primary tags that EXIF already provides.
347                // Exception: a few maker notes carry a more precise authoritative value
348                // (e.g. Kodak FNumber/ExposureTime) that ExifTool reports over EXIF — keep
349                // those so the later precedence pass can promote them.
350                for mn_tag in mn_tags {
351                    let authoritative = mn_tag.group.family1 == "Kodak"
352                        && matches!(mn_tag.name.as_str(), "FNumber" | "ExposureTime");
353                    if !authoritative
354                        && exif_primary.contains(&mn_tag.name.as_str())
355                        && exif_has.contains(&mn_tag.name)
356                    {
357                        // EXIF wins - don't add MakerNotes version
358                        continue;
359                    }
360                    tags.push(mn_tag);
361                }
362            }
363        }
364
365        // DNG PrivateData (0xC634): parse Adobe MakN for MakerNotes if no MakerNote found
366        if mn_info.is_none() {
367            // Scan for DNGPrivateData in tags — look for "Adobe\0" header
368            // Find the offset from the tag value (stored as binary/undefined)
369            Self::parse_dng_private_data(data, &header, &make, &make_and_model, &mut tags);
370        }
371
372        // Parse IPTC data embedded in TIFF (tag 0x83BB "IPTC-NAA")
373        // The raw tag stores IPTC data as undefined bytes or a list of u32 values
374        {
375            let iptc_data: Option<Vec<u8>> =
376                tags.iter().find(|t| t.name == "IPTC-NAA").and_then(|t| {
377                    match &t.raw_value {
378                        Value::Undefined(bytes) => Some(bytes.clone()),
379                        Value::Binary(bytes) => Some(bytes.clone()),
380                        Value::List(items) => {
381                            // IPTC-NAA stored as uint32 list - convert back to bytes (big-endian)
382                            let mut bytes = Vec::with_capacity(items.len() * 4);
383                            for item in items {
384                                if let Value::U32(v) = item {
385                                    bytes.extend_from_slice(&v.to_be_bytes())
386                                }
387                            }
388                            if bytes.is_empty() {
389                                None
390                            } else {
391                                Some(bytes)
392                            }
393                        }
394                        _ => None,
395                    }
396                });
397
398            if let Some(iptc_bytes) = iptc_data {
399                // Compute MD5 of the raw IPTC data for CurrentIPTCDigest
400                let md5_hex = crate::md5::md5_hex(&iptc_bytes);
401
402                if let Ok(iptc_tags) = crate::metadata::IptcReader::read(&iptc_bytes) {
403                    // Replace raw IPTC-NAA tag with parsed IPTC tags
404                    tags.retain(|t| t.name != "IPTC-NAA");
405                    tags.extend(iptc_tags);
406                }
407
408                // Add CurrentIPTCDigest tag
409                tags.push(crate::tag::Tag {
410                    id: crate::tag::TagId::Text("CurrentIPTCDigest".into()),
411                    name: "CurrentIPTCDigest".into(),
412                    description: "Current IPTC Digest".into(),
413                    group: crate::tag::TagGroup {
414                        family0: "IPTC".into(),
415                        family1: "IPTC".into(),
416                        family2: "Other".into(),
417                    },
418                    raw_value: Value::String(md5_hex.clone()),
419                    print_value: md5_hex,
420                    priority: 0,
421                });
422            }
423        }
424
425        // Parse ICC_Profile data embedded in TIFF (tag 0x8773)
426        {
427            let icc_data: Option<Vec<u8>> =
428                tags.iter()
429                    .find(|t| t.name == "ICC_Profile")
430                    .and_then(|t| match &t.raw_value {
431                        Value::Undefined(bytes) => Some(bytes.clone()),
432                        Value::Binary(bytes) => Some(bytes.clone()),
433                        _ => None,
434                    });
435
436            if let Some(icc_bytes) = icc_data {
437                if let Ok(icc_tags) = crate::formats::icc::read_icc(&icc_bytes) {
438                    // Replace raw ICC_Profile tag with parsed ICC tags
439                    tags.retain(|t| t.name != "ICC_Profile");
440                    tags.extend(icc_tags);
441                }
442            }
443        }
444
445        // Process GeoTIFF key directory if present
446        process_geotiff_keys(&mut tags);
447
448        // Final deduplication: within MakerNotes, if the same tag name appears multiple times
449        // (e.g., from different sub-tables), keep the last occurrence.
450        // Only deduplicate MakerNotes tags (family0 == "MakerNotes") to avoid affecting
451        // structural EXIF/IFD tags.
452        {
453            // Find MakerNotes tags that have duplicates
454            let mn_tags_start = tags
455                .iter()
456                .position(|t| t.group.family0 == "MakerNotes")
457                .unwrap_or(tags.len());
458            if mn_tags_start < tags.len() {
459                // For each MakerNotes tag name, find the last occurrence index
460                let mut last_idx: std::collections::HashMap<&str, usize> =
461                    std::collections::HashMap::new();
462                for (i, t) in tags[mn_tags_start..].iter().enumerate() {
463                    last_idx.insert(t.name.as_str(), i + mn_tags_start);
464                }
465                // Retain only the last occurrence of each MakerNotes duplicate
466                let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
467                // Process in reverse, keeping only the last (= rightmost = first in reverse)
468                let mut keep = vec![false; tags.len()];
469                for (i, t) in tags.iter().enumerate().rev() {
470                    if t.group.family0 != "MakerNotes" {
471                        keep[i] = true;
472                        continue;
473                    }
474                    if seen.insert(t.name.as_str()) {
475                        keep[i] = true; // first seen in reverse = last occurrence
476                    }
477                }
478                let mut iter = keep.iter();
479                tags.retain(|_| *iter.next().unwrap_or(&true));
480            }
481        }
482
483        // GPSLatitude/GPSLongitude print as "D deg M' S.SS\" REF" (ExifTool ToDMS),
484        // normalising fractional minutes into seconds and appending the hemisphere ref.
485        for (coord, reftag) in [
486            ("GPSLatitude", "GPSLatitudeRef"),
487            ("GPSLongitude", "GPSLongitudeRef"),
488        ] {
489            let r = tags
490                .iter()
491                .find(|t| t.name == reftag)
492                .map(|t| t.raw_value.to_display_string())
493                .and_then(|s| s.chars().next())
494                .filter(|c| "NSEW".contains(*c));
495            if let Some(refc) = r {
496                if let Some(t) = tags.iter_mut().find(|t| t.name == coord) {
497                    let parts: Vec<f64> = t
498                        .raw_value
499                        .to_display_string()
500                        .split_whitespace()
501                        .filter_map(|s| s.parse::<f64>().ok())
502                        .collect();
503                    if parts.len() == 3 {
504                        let dec = parts[0] + parts[1] / 60.0 + parts[2] / 3600.0;
505                        let deg = dec.floor();
506                        let rem = (dec - deg) * 60.0;
507                        let min = rem.floor();
508                        let sec = (rem - min) * 60.0;
509                        t.print_value =
510                            format!("{} deg {}' {:.2}\" {}", deg as i64, min as i64, sec, refc);
511                    }
512                }
513            }
514            // Undefined coordinate rationals (0/0 0/0 0/0) yield an empty value.
515            if let Some(t) = tags.iter_mut().find(|t| t.name == coord) {
516                if t.print_value.split_whitespace().all(|p| p == "undef") {
517                    t.print_value = String::new();
518                }
519            }
520        }
521
522        // ExifTool uses the full-resolution sub-IFD (SubfileType = "Full-resolution
523        // image") for the primary image dimensions. When such a sub-IFD exists (e.g. the
524        // real raw image in a DNG/NEF whose IFD0 is a small reduced-resolution preview),
525        // promote its ImageWidth/ImageHeight to the front so they win first-by-name and
526        // feed the ImageSize/Megapixels composites.
527        let fullres_group = tags
528            .iter()
529            .find(|t| {
530                t.name == "SubfileType"
531                    && t.print_value == "Full-resolution image"
532                    && t.group.family1 != "IFD0"
533            })
534            .map(|t| t.group.family1.clone());
535        if let Some(group) = fullres_group {
536            for dim in ["ImageHeight", "ImageWidth"] {
537                if let Some(pos) = tags
538                    .iter()
539                    .position(|t| t.name == dim && t.group.family1 == group)
540                {
541                    let t = tags.remove(pos);
542                    tags.insert(0, t);
543                }
544            }
545        }
546
547        Ok(tags)
548    }
549
550    /// Find MakerNote (tag 0x927C) offset and size in ExifIFD.
551    /// Parse DNG PrivateData (0xC634) to extract embedded MakerNotes
552    fn parse_dng_private_data(
553        data: &[u8],
554        header: &TiffHeader,
555        make: &str,
556        model: &str,
557        tags: &mut Vec<Tag>,
558    ) {
559        // Scan IFD0 for tag 0xC634
560        let ifd0_offset = header.ifd0_offset as usize;
561        if ifd0_offset + 2 > data.len() {
562            return;
563        }
564        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
565        let entries_start = ifd0_offset + 2;
566        for i in 0..entry_count {
567            let eoff = entries_start + i * 12;
568            if eoff + 12 > data.len() {
569                break;
570            }
571            let tag = read_u16(data, eoff, header.byte_order);
572            if tag == 0xC634 {
573                let dtype = read_u16(data, eoff + 2, header.byte_order);
574                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
575                let elem_size = match dtype {
576                    1 | 7 => 1,
577                    _ => 0,
578                };
579                let total = elem_size * count;
580                if total < 14 {
581                    continue;
582                }
583                let off = read_u32(data, eoff + 8, header.byte_order) as usize;
584                if off + total > data.len() {
585                    continue;
586                }
587                let pdata = &data[off..off + total];
588                // Parse Adobe DNGPrivateData: "Adobe\0" + blocks
589                if !pdata.starts_with(b"Adobe\0") {
590                    continue;
591                }
592                let mut bpos = 6;
593                while bpos + 8 <= pdata.len() {
594                    let btag = &pdata[bpos..bpos + 4];
595                    let bsize = u32::from_be_bytes([
596                        pdata[bpos + 4],
597                        pdata[bpos + 5],
598                        pdata[bpos + 6],
599                        pdata[bpos + 7],
600                    ]) as usize;
601                    bpos += 8;
602                    if bpos + bsize > pdata.len() {
603                        break;
604                    }
605                    if btag == b"MakN" && bsize > 6 {
606                        let mn_block = &pdata[bpos..bpos + bsize];
607                        let mn_bo = if &mn_block[0..2] == b"II" {
608                            ByteOrderMark::LittleEndian
609                        } else {
610                            ByteOrderMark::BigEndian
611                        };
612                        let mut mn_start = 6; // skip byte order + original offset
613                                              // Hack for extra 12 bytes in MakN header (Adobe Camera Raw bug)
614                        if bsize >= 18 && &mn_block[6..10] == b"\0\0\0\x01" {
615                            mn_start += 12;
616                        }
617                        if mn_start < bsize {
618                            // Emit MakerNoteByteOrder
619                            let mn_bo_str = if mn_bo == ByteOrderMark::LittleEndian {
620                                "Little-endian (Intel, II)"
621                            } else {
622                                "Big-endian (Motorola, MM)"
623                            };
624                            tags.push(Tag {
625                                id: TagId::Text("MakerNoteByteOrder".into()),
626                                name: "MakerNoteByteOrder".into(),
627                                description: "Maker Note Byte Order".into(),
628                                group: TagGroup {
629                                    family0: "File".into(),
630                                    family1: "File".into(),
631                                    family2: "Image".into(),
632                                },
633                                raw_value: Value::String(mn_bo_str.into()),
634                                print_value: mn_bo_str.into(),
635                                priority: 0,
636                            });
637                            // Canon MakerNotes have a TIFF footer with the original offset.
638                            // Sub-table value_offsets are relative to the original file.
639                            // We need to pass the full DNG data so offsets resolve correctly.
640                            let mn_data_in_block = &mn_block[mn_start..];
641                            let mn_abs_offset = off + (bpos - 8 + 8) + mn_start; // absolute offset in DNG file
642                                                                                 // Check for Canon TIFF footer (last 8 bytes)
643                            let fix_base = if mn_data_in_block.len() > 8 {
644                                let footer = &mn_data_in_block[mn_data_in_block.len() - 8..];
645                                if (footer[0..2] == *b"II" || footer[0..2] == *b"MM")
646                                    && (footer[2..4] == *b"\x2a\x00"
647                                        || footer[2..4] == *b"\x00\x2a")
648                                {
649                                    let old_off = if footer[0] == b'I' {
650                                        u32::from_le_bytes([
651                                            footer[4], footer[5], footer[6], footer[7],
652                                        ])
653                                    } else {
654                                        u32::from_be_bytes([
655                                            footer[4], footer[5], footer[6], footer[7],
656                                        ])
657                                    } as usize;
658                                    if old_off > 0 && mn_abs_offset > old_off {
659                                        mn_abs_offset as isize - old_off as isize
660                                    } else {
661                                        0
662                                    }
663                                } else {
664                                    0
665                                }
666                            } else {
667                                0
668                            };
669
670                            let mn_tags = if fix_base != 0 {
671                                // Pass full DNG data with corrected offset and base fix
672                                crate::metadata::makernotes::parse_makernotes_with_base(
673                                    data,
674                                    mn_abs_offset,
675                                    mn_data_in_block.len(),
676                                    make,
677                                    model,
678                                    mn_bo,
679                                    fix_base,
680                                )
681                            } else {
682                                crate::metadata::makernotes::parse_makernotes(
683                                    mn_data_in_block,
684                                    0,
685                                    mn_data_in_block.len(),
686                                    make,
687                                    model,
688                                    mn_bo,
689                                )
690                            };
691                            // DNG MakerNote tags that Perl doesn't emit (conditions/Unknown/offset issues)
692                            let dng_suppress = [
693                                "AESetting",
694                                "CameraISO",
695                                "ImageStabilization",
696                                "SpotMeteringMode",
697                                "RawJpgSize",
698                                "Warning",
699                            ];
700                            for mn_tag in mn_tags {
701                                if dng_suppress.contains(&mn_tag.name.as_str()) {
702                                    continue;
703                                }
704                                let exists = tags.iter().any(|t| t.name == mn_tag.name);
705                                if exists {
706                                    // EXIF-primary tags: EXIF wins, skip MakerNotes.
707                                    // Other tags: MakerNotes wins — drop the existing
708                                    // EXIF/XMP duplicates and add the MakerNotes version.
709                                    if EXIF_PRIMARY_TAGS.contains(&mn_tag.name.as_str()) {
710                                        continue;
711                                    }
712                                    tags.retain(|t| t.name != mn_tag.name);
713                                }
714                                tags.push(mn_tag);
715                            }
716                        }
717                    }
718                    bpos += bsize;
719                }
720                break;
721            }
722        }
723    }
724
725    fn find_makernote(data: &[u8], header: &TiffHeader, result: &mut Option<(usize, usize)>) {
726        // First find ExifIFD offset from IFD0
727        let ifd0_offset = header.ifd0_offset as usize;
728        if ifd0_offset + 2 > data.len() {
729            return;
730        }
731        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
732        let entries_start = ifd0_offset + 2;
733
734        for i in 0..entry_count {
735            let eoff = entries_start + i * 12;
736            if eoff + 12 > data.len() {
737                break;
738            }
739            let tag = read_u16(data, eoff, header.byte_order);
740            if tag == 0x8769 {
741                // ExifIFD pointer
742                let exif_offset = read_u32(data, eoff + 8, header.byte_order) as usize;
743                Self::find_makernote_in_ifd(data, header, exif_offset, result);
744                break;
745            }
746        }
747    }
748
749    fn find_makernote_in_ifd(
750        data: &[u8],
751        header: &TiffHeader,
752        ifd_offset: usize,
753        result: &mut Option<(usize, usize)>,
754    ) {
755        if ifd_offset + 2 > data.len() {
756            return;
757        }
758        let entry_count = read_u16(data, ifd_offset, header.byte_order) as usize;
759        let entries_start = ifd_offset + 2;
760
761        for i in 0..entry_count {
762            let eoff = entries_start + i * 12;
763            if eoff + 12 > data.len() {
764                break;
765            }
766            let tag = read_u16(data, eoff, header.byte_order);
767            if tag == 0x927C {
768                let data_type = read_u16(data, eoff + 2, header.byte_order);
769                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
770                let type_size = match data_type {
771                    1 | 2 | 6 | 7 => 1,
772                    3 | 8 => 2,
773                    4 | 9 | 11 | 13 => 4,
774                    5 | 10 | 12 => 8,
775                    _ => 1,
776                };
777                let total_size = type_size * count;
778
779                if total_size <= 4 {
780                    // Inline - too small for real MakerNotes
781                    break;
782                }
783                let offset = read_u32(data, eoff + 8, header.byte_order) as usize;
784                if offset + total_size <= data.len() {
785                    *result = Some((offset, total_size));
786                }
787                break;
788            }
789        }
790    }
791
792    /// Parse EXIF data from a byte slice with an explicit byte order and offset.
793    fn read_ifd(
794        data: &[u8],
795        header: &TiffHeader,
796        offset: u32,
797        ifd_name: &str,
798        tags: &mut Vec<Tag>,
799    ) -> Result<Option<u32>> {
800        let offset = offset as usize;
801        if offset + 2 > data.len() {
802            return Err(Error::InvalidExif(format!(
803                "{} offset {} beyond data length {}",
804                ifd_name,
805                offset,
806                data.len()
807            )));
808        }
809
810        let entry_count = read_u16(data, offset, header.byte_order) as usize;
811        let entries_start = offset + 2;
812        let _entries_end = entries_start + entry_count * 12;
813
814        // Validate: at minimum, first entry must fit
815        if entries_start + 12 > data.len() && entry_count > 0 {
816            return Err(Error::InvalidExif(format!(
817                "{} entries extend beyond data (need {}, have {})",
818                ifd_name,
819                entries_start + 12,
820                data.len()
821            )));
822        }
823        // Clamp entry count if IFD extends beyond data
824        let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
825        let entries_end = entries_start + entry_count * 12;
826
827        for i in 0..entry_count {
828            let entry_offset = entries_start + i * 12;
829            let entry = parse_ifd_entry(data, entry_offset, header.byte_order);
830
831            // Check for sub-IFDs (ExifIFD, GPS, Interop)
832            match entry.tag {
833                0x8769 => {
834                    // ExifIFD
835                    let sub_offset = entry.value_offset;
836                    if (sub_offset as usize) < data.len() {
837                        let _ = Self::read_ifd(data, header, sub_offset, "ExifIFD", tags);
838                    }
839                    continue;
840                }
841                0x8825 => {
842                    // GPS IFD
843                    let sub_offset = entry.value_offset;
844                    if (sub_offset as usize) < data.len() {
845                        let _ = Self::read_ifd(data, header, sub_offset, "GPS", tags);
846                    }
847                    continue;
848                }
849                0xA005 => {
850                    // Interop IFD
851                    let sub_offset = entry.value_offset;
852                    if (sub_offset as usize) < data.len() {
853                        let _ = Self::read_ifd(data, header, sub_offset, "InteropIFD", tags);
854                    }
855                    continue;
856                }
857                // PrintIM tag: extract version from "PrintIM" + 4-byte version
858                0xC4A5 => {
859                    let total_size = match entry.data_type {
860                        1 | 2 | 6 | 7 => entry.count as usize,
861                        _ => 0,
862                    };
863                    if total_size >= 12 {
864                        let off = entry.value_offset as usize;
865                        if off + 12 <= data.len() && &data[off..off + 7] == b"PrintIM" {
866                            // "PrintIM\0" is 8 bytes; the 4-byte version follows.
867                            let ver =
868                                crate::encoding::decode_utf8_or_latin1(&data[off + 8..off + 12])
869                                    .trim_end_matches('\0')
870                                    .to_string();
871                            tags.push(Tag {
872                                id: TagId::Text("PrintIMVersion".into()),
873                                name: "PrintIMVersion".into(),
874                                description: "PrintIM Version".into(),
875                                group: TagGroup {
876                                    family0: "PrintIM".into(),
877                                    family1: "PrintIM".into(),
878                                    family2: "Printing".into(),
879                                },
880                                raw_value: Value::String(ver.clone()),
881                                print_value: ver,
882                                priority: 0,
883                            });
884                        }
885                    }
886                    continue; // Suppress raw PrintIM tag
887                }
888                // Suppress GPS tag 0x0006 (GPSAltitude) when value is 0/0
889                0x0006 if ifd_name == "GPS" => {
890                    if let Some(Value::URational(0, 0)) =
891                        read_ifd_value(data, &entry, header.byte_order)
892                    {
893                        continue;
894                    }
895                }
896                // In SubIFD, tag 0x0201 = JpgFromRawStart (JPEG preview offset)
897                0x0201 if ifd_name.starts_with("SubIFD") => {
898                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
899                        let pv = val.to_display_string();
900                        tags.push(Tag {
901                            id: TagId::Numeric(entry.tag),
902                            name: "JpgFromRawStart".into(),
903                            description: "Jpg From Raw Start".into(),
904                            group: TagGroup {
905                                family0: "EXIF".into(),
906                                family1: ifd_name.to_string(),
907                                family2: "Image".into(),
908                            },
909                            raw_value: val,
910                            print_value: pv,
911                            priority: 0,
912                        });
913                    }
914                    continue;
915                }
916                // In SubIFD, tag 0x0202 = JpgFromRawLength (JPEG preview byte count)
917                0x0202 if ifd_name.starts_with("SubIFD") => {
918                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
919                        let pv = val.to_display_string();
920                        tags.push(Tag {
921                            id: TagId::Numeric(entry.tag),
922                            name: "JpgFromRawLength".into(),
923                            description: "Jpg From Raw Length".into(),
924                            group: TagGroup {
925                                family0: "EXIF".into(),
926                                family1: ifd_name.to_string(),
927                                family2: "Image".into(),
928                            },
929                            raw_value: val,
930                            print_value: pv,
931                            priority: 0,
932                        });
933                    }
934                    continue;
935                }
936                // SubIFD pointer (0x014A): follow to read SubIFD entries
937                0x014A if ifd_name == "IFD0" => {
938                    // Read SubIFD offset(s) — may be a single uint32 or array
939                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
940                        let offsets: Vec<u32> = match &val {
941                            Value::U32(v) => vec![*v],
942                            Value::List(items) => items
943                                .iter()
944                                .filter_map(|v| {
945                                    if let Value::U32(o) = v {
946                                        Some(*o)
947                                    } else {
948                                        None
949                                    }
950                                })
951                                .collect(),
952                            _ => vec![],
953                        };
954                        for (idx, &off) in offsets.iter().enumerate() {
955                            if (off as usize) < data.len() {
956                                let sub_name = format!("SubIFD{}", idx);
957                                let before_idx = tags.len();
958                                let _ = Self::read_ifd(data, header, off, &sub_name, tags);
959
960                                // Check if this SubIFD has JPEG compression
961                                let is_jpeg = tags[before_idx..].iter().any(|t| {
962                                    t.name == "Compression"
963                                        && (t.print_value.contains("JPEG")
964                                            || t.raw_value.as_u64() == Some(6))
965                                });
966
967                                if is_jpeg {
968                                    // Rename StripOffsets/StripByteCounts based on SubIFD index
969                                    // Perl: SubIFD2 → JpgFromRaw*, others → PreviewImage*
970                                    let (start_name, len_name, img_name) = if idx == 2 {
971                                        ("JpgFromRawStart", "JpgFromRawLength", "JpgFromRaw")
972                                    } else {
973                                        ("PreviewImageStart", "PreviewImageLength", "PreviewImage")
974                                    };
975                                    // Find StripOffsets and StripByteCounts in this SubIFD
976                                    let strip_off = tags[before_idx..]
977                                        .iter()
978                                        .find(|t| t.name == "StripOffsets")
979                                        .and_then(|t| t.raw_value.as_u64());
980                                    let strip_len = tags[before_idx..]
981                                        .iter()
982                                        .find(|t| t.name == "StripByteCounts")
983                                        .and_then(|t| t.raw_value.as_u64());
984                                    if let (Some(s), Some(l)) = (strip_off, strip_len) {
985                                        tags.push(Tag {
986                                            id: TagId::Text(start_name.into()),
987                                            name: start_name.into(),
988                                            description: start_name.into(),
989                                            group: TagGroup {
990                                                family0: "EXIF".into(),
991                                                family1: sub_name.clone(),
992                                                family2: "Preview".into(),
993                                            },
994                                            raw_value: Value::U32(s as u32),
995                                            print_value: s.to_string(),
996                                            priority: 0,
997                                        });
998                                        tags.push(Tag {
999                                            id: TagId::Text(len_name.into()),
1000                                            name: len_name.into(),
1001                                            description: len_name.into(),
1002                                            group: TagGroup {
1003                                                family0: "EXIF".into(),
1004                                                family1: sub_name.clone(),
1005                                                family2: "Preview".into(),
1006                                            },
1007                                            raw_value: Value::U32(l as u32),
1008                                            print_value: l.to_string(),
1009                                            priority: 0,
1010                                        });
1011                                        // Extract binary image data
1012                                        let s = s as usize;
1013                                        let l = l as usize;
1014                                        if l > 0 && s + l <= data.len() {
1015                                            let pv = format!(
1016                                                "(Binary data {} bytes, use -b option to extract)",
1017                                                l
1018                                            );
1019                                            tags.push(Tag {
1020                                                id: TagId::Text(img_name.into()),
1021                                                name: img_name.into(),
1022                                                description: img_name.into(),
1023                                                group: TagGroup {
1024                                                    family0: "EXIF".into(),
1025                                                    family1: sub_name.clone(),
1026                                                    family2: "Preview".into(),
1027                                                },
1028                                                raw_value: Value::Binary(data[s..s + l].to_vec()),
1029                                                print_value: pv,
1030                                                priority: 0,
1031                                            });
1032                                        }
1033                                    }
1034                                }
1035
1036                                // Legacy: Also check for already-named JpgFromRawStart tags
1037                                let jpg_start = tags[before_idx..]
1038                                    .iter()
1039                                    .find(|t| t.name == "JpgFromRawStart")
1040                                    .and_then(|t| t.raw_value.as_u64());
1041                                let jpg_len = tags[before_idx..]
1042                                    .iter()
1043                                    .find(|t| t.name == "JpgFromRawLength")
1044                                    .and_then(|t| t.raw_value.as_u64());
1045                                if let (Some(start), Some(len)) = (jpg_start, jpg_len) {
1046                                    let start = start as usize;
1047                                    let len = len as usize;
1048                                    if len > 0 && start + len <= data.len() {
1049                                        let pv = format!(
1050                                            "(Binary data {} bytes, use -b option to extract)",
1051                                            len
1052                                        );
1053                                        tags.push(Tag {
1054                                            id: TagId::Text("JpgFromRaw".into()),
1055                                            name: "JpgFromRaw".into(),
1056                                            description: "Jpg From Raw".into(),
1057                                            group: TagGroup {
1058                                                family0: "EXIF".into(),
1059                                                family1: sub_name,
1060                                                family2: "Preview".into(),
1061                                            },
1062                                            raw_value: Value::Binary(
1063                                                data[start..start + len].to_vec(),
1064                                            ),
1065                                            print_value: pv,
1066                                            priority: 0,
1067                                        });
1068                                    }
1069                                }
1070                            }
1071                        }
1072                    }
1073                    continue;
1074                }
1075                // In CR2 IFD2 (preview JPEG), suppress StripOffsets/StripByteCounts
1076                // because IFD3 has the correct values for the raw data.
1077                // Also suppress tags that duplicate IFD0 content (ImageWidth, ImageHeight,
1078                // BitsPerSample, Compression) since the first (IFD0) value is preferred.
1079                0x0100 | 0x0101 | 0x0102 | 0x0103 | 0x0111 | 0x0117 if ifd_name == "IFD2" => {
1080                    continue;
1081                }
1082                // In CR2 IFD3 (raw data), suppress Compression (IFD0 value is preferred).
1083                0x0103 if ifd_name == "IFD3" => {
1084                    continue;
1085                }
1086                _ => {}
1087            }
1088
1089            if let Some(mut value) = read_ifd_value(data, &entry, header.byte_order) {
1090                // GPS TimeStamp (0x0007): convert 0/0 rationals to 0/1 so it displays as "0, 0, 0"
1091                // (Perl treats 0/0 as 0 for GPS time, enabling GPSDateTime composite)
1092                if ifd_name == "GPS" && entry.tag == 0x0007 {
1093                    if let Value::List(ref mut items) = value {
1094                        for item in items.iter_mut() {
1095                            if matches!(item, Value::URational(0, 0)) {
1096                                *item = Value::URational(0, 1);
1097                            }
1098                        }
1099                    }
1100                }
1101                let tag_info = exif_tags::lookup(ifd_name, entry.tag);
1102                let (name, description, family2) = match tag_info {
1103                    Some(info) => (
1104                        info.name.to_string(),
1105                        info.description.to_string(),
1106                        info.family2.to_string(),
1107                    ),
1108                    None => {
1109                        // Skip known SubDirectory/internal tags that Perl doesn't emit
1110                        if matches!(
1111                            entry.tag,
1112                            // 0x014A handled above (SubIFD traversal)
1113                            // 0x02BC (ApplicationNotes) now parsed as XMP above
1114                            0xC634 // DNG PrivateData — processed after IFD scan
1115                        ) {
1116                            continue;
1117                        }
1118                        // Fallback to generated tags
1119                        match exif_tags::lookup_generated(entry.tag) {
1120                            Some((n, d)) => (n.to_string(), d.to_string(), "Other".to_string()),
1121                            None => {
1122                                // Perl doesn't emit unknown EXIF tags by default
1123                                continue;
1124                            }
1125                        }
1126                    }
1127                };
1128
1129                // Parse ApplicationNotes (0x02BC) as XMP
1130                if name == "ApplicationNotes" {
1131                    if let Value::Binary(ref xmp_bytes) = value {
1132                        if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_bytes) {
1133                            tags.extend(xmp_tags);
1134                        }
1135                    }
1136                    continue;
1137                }
1138                // Suppress known SubDirectory/internal tags
1139                if matches!(
1140                    name.as_str(),
1141                    "MinSampleValue" | "MaxSampleValue" | // Not emitted by Perl for raw formats
1142                    "ProcessingSoftware" | // Protected tag, not always emitted
1143                    "PanasonicTitle" | "PanasonicTitle2" // DNG tags, wrong match for RW2
1144                ) {
1145                    continue;
1146                }
1147
1148                let print_value = if name.starts_with("Tag0x") && get_show_unknown() >= 2 {
1149                    // -U mode: show binary data for unknown tags
1150                    match &value {
1151                        Value::Binary(bytes) | Value::Undefined(bytes) => bytes
1152                            .iter()
1153                            .map(|b| format!("{:02x}", b))
1154                            .collect::<Vec<_>>()
1155                            .join(" "),
1156                        _ => value.to_display_string(),
1157                    }
1158                } else if name.starts_with("Tag0x") {
1159                    // -u mode: show unknown tags but use standard display for values
1160                    value.to_display_string()
1161                } else {
1162                    exif_tags::print_conv(ifd_name, entry.tag, &value)
1163                        .or_else(|| {
1164                            // Fallback to generated print conversions
1165                            value
1166                                .as_u64()
1167                                .and_then(|v| {
1168                                    crate::tags::print_conv_generated::print_conv_by_name(
1169                                        &name, v as i64,
1170                                    )
1171                                })
1172                                .map(|s| s.to_string())
1173                        })
1174                        .unwrap_or_else(|| value.to_display_string())
1175                };
1176
1177                tags.push(Tag {
1178                    id: TagId::Numeric(entry.tag),
1179                    name,
1180                    description,
1181                    group: TagGroup {
1182                        family0: "EXIF".to_string(),
1183                        family1: ifd_name.to_string(),
1184                        family2,
1185                    },
1186                    raw_value: value,
1187                    print_value,
1188                    priority: 0,
1189                });
1190            }
1191        }
1192
1193        // Read next IFD offset
1194        let next_ifd_offset = if entries_end + 4 <= data.len() {
1195            read_u32(data, entries_end, header.byte_order)
1196        } else {
1197            0
1198        };
1199        if next_ifd_offset != 0 && ifd_name == "IFD0" {
1200            // IFD1 = thumbnail
1201            let ifd1_start_idx = tags.len();
1202            let ifd1_next = Self::read_ifd(data, header, next_ifd_offset, "IFD1", tags)
1203                .ok()
1204                .flatten();
1205            // Suppress IFD1 tags that duplicate IFD0 tags (only keep thumbnail-specific ones)
1206            // In Perl, IFD1 (thumbnail) tags are secondary and don't appear in output if IFD0 has them.
1207            {
1208                let ifd0_names: std::collections::HashSet<String> = tags[..ifd1_start_idx]
1209                    .iter()
1210                    .map(|t| t.name.clone())
1211                    .collect();
1212                let thumbnail_tags = [
1213                    "ThumbnailOffset",
1214                    "ThumbnailLength",
1215                    "ThumbnailImage",
1216                    "Compression",
1217                    "PhotometricInterpretation",
1218                    "JPEGInterchangeFormat",
1219                    "JPEGInterchangeFormatLength",
1220                    "SubfileType",
1221                    "StripOffsets",
1222                    "StripByteCounts",
1223                ];
1224                tags.retain(|t| {
1225                    if t.group.family1 != "IFD1" {
1226                        return true;
1227                    }
1228                    // Keep thumbnail-specific tags
1229                    if thumbnail_tags.contains(&t.name.as_str()) {
1230                        return true;
1231                    }
1232                    // Suppress IFD1 tags that IFD0 already has
1233                    !ifd0_names.contains(&t.name)
1234                });
1235            }
1236
1237            // Create ThumbnailImage tag if offset+length are present
1238            let thumb_offset = tags
1239                .iter()
1240                .find(|t| t.name == "ThumbnailOffset" && t.group.family1 == "IFD1")
1241                .and_then(|t| t.raw_value.as_u64());
1242            let thumb_length = tags
1243                .iter()
1244                .find(|t| t.name == "ThumbnailLength" && t.group.family1 == "IFD1")
1245                .and_then(|t| t.raw_value.as_u64());
1246
1247            if let (Some(off), Some(len)) = (thumb_offset, thumb_length) {
1248                let off = off as usize;
1249                let len = len as usize;
1250                if off + len <= data.len() && len > 0 {
1251                    tags.push(Tag {
1252                        id: TagId::Text("ThumbnailImage".into()),
1253                        name: "ThumbnailImage".into(),
1254                        description: "Thumbnail Image".into(),
1255                        group: TagGroup {
1256                            family0: "EXIF".into(),
1257                            family1: "IFD1".into(),
1258                            family2: "Image".into(),
1259                        },
1260                        raw_value: Value::Binary(data[off..off + len].to_vec()),
1261                        print_value: format!(
1262                            "(Binary data {} bytes, use -b option to extract)",
1263                            len
1264                        ),
1265                        priority: 0,
1266                    });
1267                }
1268            }
1269
1270            // CR2 files have additional IFDs (IFD2, IFD3) following IFD1 in the chain.
1271            // CR2 is identified by "CR" bytes at offset 8 in the TIFF data.
1272            let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
1273            if is_cr2 {
1274                if let Some(ifd2_offset) = ifd1_next {
1275                    // IFD2 = preview JPEG image data (emit selected tags)
1276                    let ifd2_next = Self::read_ifd(data, header, ifd2_offset, "IFD2", tags)
1277                        .ok()
1278                        .flatten();
1279                    // IFD3 = raw image data (emit CR2CFAPattern, RawImageSegmentation, StripOffsets, StripByteCounts)
1280                    if let Some(ifd3_offset) = ifd2_next {
1281                        let _ = Self::read_ifd(data, header, ifd3_offset, "IFD3", tags);
1282                    }
1283                }
1284            }
1285        }
1286
1287        Ok(if next_ifd_offset != 0 {
1288            Some(next_ifd_offset)
1289        } else {
1290            None
1291        })
1292    }
1293
1294    /// Parse a TIFF where IFD0 is treated as a named IFD (e.g. "GPS", "ExifIFD").
1295    /// Used for CR3 CMT4 (GPS-only TIFF) and CMT2 (ExifIFD-only TIFF).
1296    /// Does NOT emit ExifByteOrder or do MakerNote/IFD1 processing.
1297    pub fn read_as_named_ifd(data: &[u8], ifd_name: &str) -> Vec<Tag> {
1298        let header = match parse_tiff_header(data) {
1299            Ok(h) => h,
1300            Err(_) => return Vec::new(),
1301        };
1302        let mut tags = Vec::new();
1303        let _ = Self::read_ifd(data, &header, header.ifd0_offset, ifd_name, &mut tags);
1304        tags
1305    }
1306}
1307
1308fn parse_ifd_entry(data: &[u8], offset: usize, byte_order: ByteOrderMark) -> IfdEntry {
1309    let tag = read_u16(data, offset, byte_order);
1310    let data_type = read_u16(data, offset + 2, byte_order);
1311    let count = read_u32(data, offset + 4, byte_order);
1312    let value_offset = read_u32(data, offset + 8, byte_order);
1313    let mut inline_data = [0u8; 4];
1314    inline_data.copy_from_slice(&data[offset + 8..offset + 12]);
1315
1316    IfdEntry {
1317        tag,
1318        data_type,
1319        count,
1320        value_offset,
1321        inline_data,
1322    }
1323}
1324
1325fn read_ifd_value(data: &[u8], entry: &IfdEntry, byte_order: ByteOrderMark) -> Option<Value> {
1326    let elem_size = type_size(entry.data_type)?;
1327    let total_size = elem_size * entry.count as usize;
1328
1329    let value_data = if total_size <= 4 {
1330        &entry.inline_data[..total_size]
1331    } else {
1332        let offset = entry.value_offset as usize;
1333        if offset + total_size > data.len() {
1334            return None;
1335        }
1336        &data[offset..offset + total_size]
1337    };
1338
1339    // IPTC-NAA (0x83BB): always read as raw binary regardless of declared type
1340    if entry.tag == 0x83BB {
1341        return Some(Value::Binary(value_data.to_vec()));
1342    }
1343
1344    // ApplicationNotes (0x02BC): always read as raw binary (XMP data)
1345    if entry.tag == 0x02BC {
1346        return Some(Value::Binary(value_data.to_vec()));
1347    }
1348
1349    match entry.data_type {
1350        // BYTE
1351        1 => {
1352            if entry.count == 1 {
1353                Some(Value::U8(value_data[0]))
1354            } else {
1355                Some(Value::List(
1356                    value_data.iter().map(|&b| Value::U8(b)).collect(),
1357                ))
1358            }
1359        }
1360        // ASCII
1361        2 => {
1362            let s = crate::encoding::decode_utf8_or_latin1(value_data);
1363            // ExifTool truncates at the first null (s/\0.*//s) and drops trailing blanks
1364            // padding the fixed-width field (e.g. "CASIO COMPUTER CO.,LTD ").
1365            let s = s.split('\0').next().unwrap_or("").trim_end().to_string();
1366            Some(Value::String(s))
1367        }
1368        // SHORT
1369        3 => {
1370            if entry.count == 1 {
1371                Some(Value::U16(read_u16(value_data, 0, byte_order)))
1372            } else {
1373                let vals: Vec<Value> = (0..entry.count as usize)
1374                    .map(|i| Value::U16(read_u16(value_data, i * 2, byte_order)))
1375                    .collect();
1376                Some(Value::List(vals))
1377            }
1378        }
1379        // LONG
1380        4 | 13 => {
1381            if entry.count == 1 {
1382                Some(Value::U32(read_u32(value_data, 0, byte_order)))
1383            } else {
1384                let vals: Vec<Value> = (0..entry.count as usize)
1385                    .map(|i| Value::U32(read_u32(value_data, i * 4, byte_order)))
1386                    .collect();
1387                Some(Value::List(vals))
1388            }
1389        }
1390        // RATIONAL (unsigned)
1391        5 => {
1392            if entry.count == 1 {
1393                let n = read_u32(value_data, 0, byte_order);
1394                let d = read_u32(value_data, 4, byte_order);
1395                Some(Value::URational(n, d))
1396            } else {
1397                let vals: Vec<Value> = (0..entry.count as usize)
1398                    .map(|i| {
1399                        let n = read_u32(value_data, i * 8, byte_order);
1400                        let d = read_u32(value_data, i * 8 + 4, byte_order);
1401                        Value::URational(n, d)
1402                    })
1403                    .collect();
1404                Some(Value::List(vals))
1405            }
1406        }
1407        // SBYTE
1408        6 => {
1409            if entry.count == 1 {
1410                Some(Value::I16(value_data[0] as i8 as i16))
1411            } else {
1412                let vals: Vec<Value> = value_data
1413                    .iter()
1414                    .map(|&b| Value::I16(b as i8 as i16))
1415                    .collect();
1416                Some(Value::List(vals))
1417            }
1418        }
1419        // UNDEFINED
1420        7 => Some(Value::Undefined(value_data.to_vec())),
1421        // SSHORT
1422        8 => {
1423            if entry.count == 1 {
1424                Some(Value::I16(read_i16(value_data, 0, byte_order)))
1425            } else {
1426                let vals: Vec<Value> = (0..entry.count as usize)
1427                    .map(|i| Value::I16(read_i16(value_data, i * 2, byte_order)))
1428                    .collect();
1429                Some(Value::List(vals))
1430            }
1431        }
1432        // SLONG
1433        9 => {
1434            if entry.count == 1 {
1435                Some(Value::I32(read_i32(value_data, 0, byte_order)))
1436            } else {
1437                let vals: Vec<Value> = (0..entry.count as usize)
1438                    .map(|i| Value::I32(read_i32(value_data, i * 4, byte_order)))
1439                    .collect();
1440                Some(Value::List(vals))
1441            }
1442        }
1443        // SRATIONAL
1444        10 => {
1445            if entry.count == 1 {
1446                let n = read_i32(value_data, 0, byte_order);
1447                let d = read_i32(value_data, 4, byte_order);
1448                Some(Value::IRational(n, d))
1449            } else {
1450                let vals: Vec<Value> = (0..entry.count as usize)
1451                    .map(|i| {
1452                        let n = read_i32(value_data, i * 8, byte_order);
1453                        let d = read_i32(value_data, i * 8 + 4, byte_order);
1454                        Value::IRational(n, d)
1455                    })
1456                    .collect();
1457                Some(Value::List(vals))
1458            }
1459        }
1460        // FLOAT
1461        11 => {
1462            if entry.count == 1 {
1463                let bits = read_u32(value_data, 0, byte_order);
1464                Some(Value::F32(f32::from_bits(bits)))
1465            } else {
1466                let vals: Vec<Value> = (0..entry.count as usize)
1467                    .map(|i| {
1468                        let bits = read_u32(value_data, i * 4, byte_order);
1469                        Value::F32(f32::from_bits(bits))
1470                    })
1471                    .collect();
1472                Some(Value::List(vals))
1473            }
1474        }
1475        // DOUBLE
1476        12 => {
1477            if entry.count == 1 {
1478                let bits = read_u64(value_data, 0, byte_order);
1479                Some(Value::F64(f64::from_bits(bits)))
1480            } else {
1481                let vals: Vec<Value> = (0..entry.count as usize)
1482                    .map(|i| {
1483                        let bits = read_u64(value_data, i * 8, byte_order);
1484                        Value::F64(f64::from_bits(bits))
1485                    })
1486                    .collect();
1487                Some(Value::List(vals))
1488            }
1489        }
1490        _ => None,
1491    }
1492}
1493
1494// Byte-order-aware read helpers
1495fn read_u16(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1496    match bo {
1497        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[offset..]),
1498        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[offset..]),
1499    }
1500}
1501
1502fn read_u32(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1503    match bo {
1504        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[offset..]),
1505        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[offset..]),
1506    }
1507}
1508
1509fn read_u64(data: &[u8], offset: usize, bo: ByteOrderMark) -> u64 {
1510    match bo {
1511        ByteOrderMark::LittleEndian => LittleEndian::read_u64(&data[offset..]),
1512        ByteOrderMark::BigEndian => BigEndian::read_u64(&data[offset..]),
1513    }
1514}
1515
1516fn read_i16(data: &[u8], offset: usize, bo: ByteOrderMark) -> i16 {
1517    match bo {
1518        ByteOrderMark::LittleEndian => LittleEndian::read_i16(&data[offset..]),
1519        ByteOrderMark::BigEndian => BigEndian::read_i16(&data[offset..]),
1520    }
1521}
1522
1523fn read_i32(data: &[u8], offset: usize, bo: ByteOrderMark) -> i32 {
1524    match bo {
1525        ByteOrderMark::LittleEndian => LittleEndian::read_i32(&data[offset..]),
1526        ByteOrderMark::BigEndian => BigEndian::read_i32(&data[offset..]),
1527    }
1528}
1529
1530/// Process GeoTIFF key directory (tag GeoTiffDirectory / GeoKeyDirectory)
1531/// and replace raw directory/ascii/double params with named GeoTIFF tags.
1532fn process_geotiff_keys(tags: &mut Vec<Tag>) {
1533    // Extract GeoTiffDirectory values
1534    let dir_vals: Option<Vec<u16>> =
1535        tags.iter()
1536            .find(|t| t.name == "GeoTiffDirectory")
1537            .and_then(|t| match &t.raw_value {
1538                Value::List(items) => {
1539                    let vals: Vec<u16> = items
1540                        .iter()
1541                        .filter_map(|v| match v {
1542                            Value::U16(x) => Some(*x),
1543                            Value::U32(x) => Some(*x as u16),
1544                            _ => None,
1545                        })
1546                        .collect();
1547                    if vals.is_empty() {
1548                        None
1549                    } else {
1550                        Some(vals)
1551                    }
1552                }
1553                _ => None,
1554            });
1555
1556    let dir_vals = match dir_vals {
1557        Some(v) => v,
1558        None => return,
1559    };
1560
1561    if dir_vals.len() < 4 {
1562        return;
1563    }
1564
1565    let version = dir_vals[0];
1566    let revision = dir_vals[1];
1567    let minor_rev = dir_vals[2];
1568    let num_entries = dir_vals[3] as usize;
1569
1570    if dir_vals.len() < 4 + num_entries * 4 {
1571        return;
1572    }
1573
1574    // Extract ASCII params
1575    let ascii_params: Option<String> = tags
1576        .iter()
1577        .find(|t| t.name == "GeoTiffAsciiParams")
1578        .map(|t| t.print_value.clone());
1579
1580    // Extract double params
1581    let double_params: Option<Vec<f64>> = tags
1582        .iter()
1583        .find(|t| t.name == "GeoTiffDoubleParams")
1584        .and_then(|t| match &t.raw_value {
1585            Value::List(items) => {
1586                let vals: Vec<f64> = items
1587                    .iter()
1588                    .filter_map(|v| match v {
1589                        Value::F64(x) => Some(*x),
1590                        Value::F32(x) => Some(*x as f64),
1591                        _ => None,
1592                    })
1593                    .collect();
1594                if vals.is_empty() {
1595                    None
1596                } else {
1597                    Some(vals)
1598                }
1599            }
1600            _ => None,
1601        });
1602
1603    let mut new_tags = Vec::new();
1604
1605    // Version tag
1606    new_tags.push(Tag {
1607        id: TagId::Text("GeoTiffVersion".to_string()),
1608        name: "GeoTiffVersion".to_string(),
1609        description: "GeoTiff Version".to_string(),
1610        group: TagGroup {
1611            family0: "EXIF".into(),
1612            family1: "IFD0".into(),
1613            family2: "Location".into(),
1614        },
1615        raw_value: Value::String(format!("{}.{}.{}", version, revision, minor_rev)),
1616        print_value: format!("{}.{}.{}", version, revision, minor_rev),
1617        priority: 0,
1618    });
1619
1620    // Process each GeoKey
1621    for i in 0..num_entries {
1622        let base = 4 + i * 4;
1623        let key_id = dir_vals[base];
1624        let location = dir_vals[base + 1];
1625        let count = dir_vals[base + 2] as usize;
1626        let value_or_offset = dir_vals[base + 3];
1627
1628        let raw_val: Option<String> = match location {
1629            0 => {
1630                // Value stored inline in value_or_offset
1631                Some(format!("{}", value_or_offset))
1632            }
1633            34737 => {
1634                // ASCII params
1635                if let Some(ref ascii) = ascii_params {
1636                    let off = value_or_offset as usize;
1637                    let end = (off + count).min(ascii.len());
1638                    if off <= end {
1639                        let s = &ascii[off..end];
1640                        // Remove trailing '|' separators
1641                        let s = s.trim_end_matches('|').trim().to_string();
1642                        Some(s)
1643                    } else {
1644                        None
1645                    }
1646                } else {
1647                    None
1648                }
1649            }
1650            34736 => {
1651                // Double params
1652                if let Some(ref doubles) = double_params {
1653                    let off = value_or_offset as usize;
1654                    if count == 1 && off < doubles.len() {
1655                        Some(format!("{}", doubles[off]))
1656                    } else if count > 1 {
1657                        let vals: Vec<String> = doubles
1658                            .iter()
1659                            .skip(off)
1660                            .take(count)
1661                            .map(|v| format!("{}", v))
1662                            .collect();
1663                        Some(vals.join(" "))
1664                    } else {
1665                        None
1666                    }
1667                } else {
1668                    None
1669                }
1670            }
1671            _ => None,
1672        };
1673
1674        let val_str = match raw_val {
1675            Some(v) => v,
1676            None => continue,
1677        };
1678
1679        // Map GeoKey ID to tag name and print value
1680        let (tag_name, print_val) = geotiff_key_to_tag(key_id, &val_str);
1681        if tag_name.is_empty() {
1682            continue;
1683        }
1684
1685        new_tags.push(Tag {
1686            id: TagId::Text(tag_name.clone()),
1687            name: tag_name.clone(),
1688            description: tag_name.clone(),
1689            group: TagGroup {
1690                family0: "EXIF".into(),
1691                family1: "IFD0".into(),
1692                family2: "Location".into(),
1693            },
1694            raw_value: Value::String(val_str),
1695            print_value: print_val,
1696            priority: 0,
1697        });
1698    }
1699
1700    if !new_tags.is_empty() {
1701        // Remove raw GeoTIFF tags
1702        tags.retain(|t| {
1703            t.name != "GeoTiffDirectory"
1704                && t.name != "GeoTiffAsciiParams"
1705                && t.name != "GeoTiffDoubleParams"
1706        });
1707        tags.extend(new_tags);
1708    }
1709}
1710
1711/// Map a GeoKey ID to (tag_name, print_value).
1712fn geotiff_key_to_tag(key_id: u16, value: &str) -> (String, String) {
1713    let val_u16: Option<u16> = value.parse().ok();
1714
1715    match key_id {
1716        // Section 6.2.1: GeoTIFF Configuration Keys
1717        0x0001 => return ("GeoTiffVersion".to_string(), value.to_string()), // not used here
1718        0x0400 => {
1719            // GTModelType
1720            let print = match val_u16 {
1721                Some(1) => "Projected".to_string(),
1722                Some(2) => "Geographic".to_string(),
1723                Some(3) => "Geocentric".to_string(),
1724                Some(32767) => "User Defined".to_string(),
1725                _ => value.to_string(),
1726            };
1727            return ("GTModelType".to_string(), print);
1728        }
1729        0x0401 => {
1730            // GTRasterType
1731            let print = match val_u16 {
1732                Some(1) => "Pixel Is Area".to_string(),
1733                Some(2) => "Pixel Is Point".to_string(),
1734                Some(32767) => "User Defined".to_string(),
1735                _ => value.to_string(),
1736            };
1737            return ("GTRasterType".to_string(), print);
1738        }
1739        0x0402 => return ("GTCitation".to_string(), value.to_string()),
1740
1741        // Section 6.2.2: Geographic CS Parameter Keys
1742        0x0800 => {
1743            return (
1744                "GeographicType".to_string(),
1745                geotiff_pcs_name(val_u16.unwrap_or(0), value),
1746            )
1747        }
1748        0x0801 => return ("GeogCitation".to_string(), value.to_string()),
1749        0x0802 => {
1750            let print = match val_u16 {
1751                Some(32767) | Some(32766) => "User Defined".to_string(),
1752                _ => value.to_string(),
1753            };
1754            return ("GeogGeodeticDatum".to_string(), print);
1755        }
1756        0x0803 => return ("GeogPrimeMeridian".to_string(), value.to_string()),
1757        0x0804 => {
1758            return (
1759                "GeogLinearUnits".to_string(),
1760                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
1761            )
1762        }
1763        0x0805 => return ("GeogLinearUnitSize".to_string(), value.to_string()),
1764        0x0806 => return ("GeogAngularUnits".to_string(), value.to_string()),
1765        0x0807 => return ("GeogAngularUnitSize".to_string(), value.to_string()),
1766        0x0808 => return ("GeogEllipsoid".to_string(), value.to_string()),
1767        0x0809 => return ("GeogSemiMajorAxis".to_string(), value.to_string()),
1768        0x080a => return ("GeogSemiMinorAxis".to_string(), value.to_string()),
1769        0x080b => return ("GeogInvFlattening".to_string(), value.to_string()),
1770        0x080c => return ("GeogAzimuthUnits".to_string(), value.to_string()),
1771        0x080d => return ("GeogPrimeMeridianLong".to_string(), value.to_string()),
1772
1773        // Section 6.2.3: Projected CS Parameter Keys
1774        0x0C00 => {
1775            // ProjectedCSType
1776            return (
1777                "ProjectedCSType".to_string(),
1778                geotiff_pcs_name(val_u16.unwrap_or(0), value),
1779            );
1780        }
1781        0x0C01 => return ("PCSCitation".to_string(), value.to_string()),
1782        0x0C02 => {
1783            // UTM zones follow a regular range in GeoTiff.pm's Projection table:
1784            // 16001-16060 = UTM zone N (north), 16101-16160 = UTM zone N (south).
1785            let print = match val_u16 {
1786                Some(v @ 16001..=16060) => format!("UTM zone {}N", v - 16000),
1787                Some(v @ 16101..=16160) => format!("UTM zone {}S", v - 16100),
1788                Some(32767) | Some(32766) => "User Defined".to_string(),
1789                _ => value.to_string(),
1790            };
1791            return ("Projection".to_string(), print);
1792        }
1793        0x0C03 => return ("ProjCoordTrans".to_string(), value.to_string()),
1794        0x0C04 => {
1795            return (
1796                "ProjLinearUnits".to_string(),
1797                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
1798            )
1799        }
1800        0x0C05 => return ("ProjLinearUnitSize".to_string(), value.to_string()),
1801        0x0C06 => return ("ProjStdParallel1".to_string(), value.to_string()),
1802        0x0C07 => return ("ProjStdParallel2".to_string(), value.to_string()),
1803        0x0C08 => return ("ProjNatOriginLong".to_string(), value.to_string()),
1804        0x0C09 => return ("ProjNatOriginLat".to_string(), value.to_string()),
1805        0x0c0a => return ("ProjFalseEasting".to_string(), value.to_string()),
1806        0x0c0b => return ("ProjFalseNorthing".to_string(), value.to_string()),
1807        0x0c0c => return ("ProjFalseOriginLong".to_string(), value.to_string()),
1808        0x0c0d => return ("ProjFalseOriginLat".to_string(), value.to_string()),
1809        0x0c0e => return ("ProjFalseOriginEasting".to_string(), value.to_string()),
1810        0x0c0f => return ("ProjFalseOriginNorthing".to_string(), value.to_string()),
1811        0x0C10 => return ("ProjCenterLong".to_string(), value.to_string()),
1812        0x0C11 => return ("ProjCenterLat".to_string(), value.to_string()),
1813        0x0C12 => return ("ProjCenterEasting".to_string(), value.to_string()),
1814        0x0C13 => return ("ProjCenterNorthing".to_string(), value.to_string()),
1815        0x0C14 => return ("ProjScaleAtNatOrigin".to_string(), value.to_string()),
1816        0x0C15 => return ("ProjScaleAtCenter".to_string(), value.to_string()),
1817        0x0C16 => return ("ProjAzimuthAngle".to_string(), value.to_string()),
1818        0x0C17 => return ("ProjStraightVertPoleLong".to_string(), value.to_string()),
1819
1820        // Section 6.2.4: Vertical CS Keys
1821        0x1000 => return ("VerticalCSType".to_string(), value.to_string()),
1822        0x1001 => return ("VerticalCitation".to_string(), value.to_string()),
1823        0x1002 => return ("VerticalDatum".to_string(), value.to_string()),
1824        0x1003 => {
1825            return (
1826                "VerticalUnits".to_string(),
1827                geotiff_linear_unit_name(val_u16.unwrap_or(0), value),
1828            )
1829        }
1830
1831        _ => {}
1832    }
1833    (String::new(), String::new())
1834}
1835
1836fn geotiff_linear_unit_name(val: u16, fallback: &str) -> String {
1837    match val {
1838        9001 => "Linear Meter".to_string(),
1839        9002 => "Linear Foot".to_string(),
1840        9003 => "Linear Foot US Survey".to_string(),
1841        9004 => "Linear Foot Modified American".to_string(),
1842        9005 => "Linear Foot Clarke".to_string(),
1843        9006 => "Linear Foot Indian".to_string(),
1844        9007 => "Linear Link".to_string(),
1845        9008 => "Linear Link Benoit".to_string(),
1846        9009 => "Linear Link Sears".to_string(),
1847        9010 => "Linear Chain Benoit".to_string(),
1848        9011 => "Linear Chain Sears".to_string(),
1849        9012 => "Linear Yard Sears".to_string(),
1850        9013 => "Linear Yard Indian".to_string(),
1851        9014 => "Linear Fathom".to_string(),
1852        9015 => "Linear Mile International Nautical".to_string(),
1853        _ => fallback.to_string(),
1854    }
1855}
1856
1857fn geotiff_pcs_name(val: u16, fallback: &str) -> String {
1858    // Common PCS codes - just return the code with description for common ones
1859    match val {
1860        26918 => "NAD83 UTM zone 18N".to_string(),
1861        26919 => "NAD83 UTM zone 19N".to_string(),
1862        32618 => "WGS84 UTM zone 18N".to_string(),
1863        32619 => "WGS84 UTM zone 19N".to_string(),
1864        4326 => "WGS 84".to_string(),
1865        4269 => "NAD83".to_string(),
1866        4267 => "NAD27".to_string(),
1867        32767 => "User Defined".to_string(),
1868        _ => fallback.to_string(),
1869    }
1870}