Skip to main content

exiftool_rs/metadata/
exif.rs

1//! EXIF/TIFF IFD metadata reader.
2//!
3//! Implements reading of TIFF IFD structures used in EXIF, GPS, and Interop metadata.
4//! Mirrors the core logic of ExifTool's Exif.pm ProcessExif function.
5
6use byteorder::{BigEndian, ByteOrder, LittleEndian};
7
8use crate::error::{Error, Result};
9use crate::tag::{Tag, TagGroup, TagId};
10use crate::tags::exif as exif_tags;
11use crate::value::Value;
12
13/// Byte order of the TIFF data.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ByteOrderMark {
16    LittleEndian,
17    BigEndian,
18}
19
20/// Parsed TIFF header.
21#[derive(Debug)]
22pub struct TiffHeader {
23    pub byte_order: ByteOrderMark,
24    pub ifd0_offset: u32,
25}
26
27/// EXIF IFD entry as read from the file.
28#[derive(Debug)]
29struct IfdEntry {
30    tag: u16,
31    data_type: u16,
32    count: u32,
33    value_offset: u32,
34    /// For values that fit in 4 bytes, the raw 4 bytes
35    inline_data: [u8; 4],
36}
37
38/// Size in bytes for each TIFF data type.
39fn type_size(data_type: u16) -> Option<usize> {
40    match data_type {
41        1 => Some(1),  // BYTE
42        2 => Some(1),  // ASCII
43        3 => Some(2),  // SHORT
44        4 => Some(4),  // LONG
45        5 => Some(8),  // RATIONAL
46        6 => Some(1),  // SBYTE
47        7 => Some(1),  // UNDEFINED
48        8 => Some(2),  // SSHORT
49        9 => Some(4),  // SLONG
50        10 => Some(8), // SRATIONAL
51        11 => Some(4), // FLOAT
52        12 => Some(8), // DOUBLE
53        13 => Some(4), // IFD
54        _ => None,
55    }
56}
57
58/// Parse a TIFF header from raw bytes.
59pub fn parse_tiff_header(data: &[u8]) -> Result<TiffHeader> {
60    if data.len() < 8 {
61        return Err(Error::InvalidTiffHeader);
62    }
63
64    let byte_order = match (data[0], data[1]) {
65        (b'I', b'I') => ByteOrderMark::LittleEndian,
66        (b'M', b'M') => ByteOrderMark::BigEndian,
67        _ => return Err(Error::InvalidTiffHeader),
68    };
69
70    let magic = match byte_order {
71        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[2..4]),
72        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[2..4]),
73    };
74
75    if magic != 42 {
76        return Err(Error::InvalidTiffHeader);
77    }
78
79    let ifd0_offset = match byte_order {
80        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[4..8]),
81        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[4..8]),
82    };
83
84    Ok(TiffHeader {
85        byte_order,
86        ifd0_offset,
87    })
88}
89
90/// EXIF metadata reader.
91pub struct ExifReader;
92
93impl ExifReader {
94    /// Parse EXIF data from a byte slice (starting at the TIFF header).
95    pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
96        let header = parse_tiff_header(data)?;
97        let mut tags = Vec::new();
98
99        // Read IFD0 (main image)
100        Self::read_ifd(data, &header, header.ifd0_offset, "IFD0", &mut tags)?;
101
102        // Extract Make + Model for MakerNotes detection and sub-table dispatch
103        let make = tags
104            .iter()
105            .find(|t| t.name == "Make")
106            .map(|t| t.print_value.clone())
107            .unwrap_or_default();
108
109        let model = tags
110            .iter()
111            .find(|t| t.name == "Model")
112            .map(|t| t.print_value.clone())
113            .unwrap_or_default();
114
115        // Store model for sub-table dispatch
116        let make_and_model = if model.is_empty() { make.clone() } else { model };
117
118        // Find and parse MakerNotes
119        // Look for the MakerNote tag (0x927C) that was stored as Undefined
120        let mn_info: Option<(usize, usize)> = {
121            // Re-scan ExifIFD for MakerNote offset/size
122            let mut result = None;
123            Self::find_makernote(data, &header, &mut result);
124            result
125        };
126
127        if let Some((mn_offset, mn_size)) = mn_info {
128            let mn_tags = crate::metadata::makernotes::parse_makernotes(
129                data, mn_offset, mn_size, &make, &make_and_model, header.byte_order,
130            );
131            // Remove the raw MakerNote tag and replace with parsed tags
132            tags.retain(|t| t.name != "MakerNote");
133            tags.extend(mn_tags);
134        }
135
136        Ok(tags)
137    }
138
139    /// Find MakerNote (tag 0x927C) offset and size in ExifIFD.
140    fn find_makernote(data: &[u8], header: &TiffHeader, result: &mut Option<(usize, usize)>) {
141        // First find ExifIFD offset from IFD0
142        let ifd0_offset = header.ifd0_offset as usize;
143        if ifd0_offset + 2 > data.len() {
144            return;
145        }
146        let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
147        let entries_start = ifd0_offset + 2;
148
149        for i in 0..entry_count {
150            let eoff = entries_start + i * 12;
151            if eoff + 12 > data.len() { break; }
152            let tag = read_u16(data, eoff, header.byte_order);
153            if tag == 0x8769 {
154                // ExifIFD pointer
155                let exif_offset = read_u32(data, eoff + 8, header.byte_order) as usize;
156                Self::find_makernote_in_ifd(data, header, exif_offset, result);
157                break;
158            }
159        }
160    }
161
162    fn find_makernote_in_ifd(data: &[u8], header: &TiffHeader, ifd_offset: usize, result: &mut Option<(usize, usize)>) {
163        if ifd_offset + 2 > data.len() {
164            return;
165        }
166        let entry_count = read_u16(data, ifd_offset, header.byte_order) as usize;
167        let entries_start = ifd_offset + 2;
168
169        for i in 0..entry_count {
170            let eoff = entries_start + i * 12;
171            if eoff + 12 > data.len() { break; }
172            let tag = read_u16(data, eoff, header.byte_order);
173            if tag == 0x927C {
174                let data_type = read_u16(data, eoff + 2, header.byte_order);
175                let count = read_u32(data, eoff + 4, header.byte_order) as usize;
176                let type_size = match data_type { 1 | 2 | 6 | 7 => 1, 3 | 8 => 2, 4 | 9 | 11 | 13 => 4, 5 | 10 | 12 => 8, _ => 1 };
177                let total_size = type_size * count;
178
179                if total_size <= 4 {
180                    // Inline - too small for real MakerNotes
181                    break;
182                }
183                let offset = read_u32(data, eoff + 8, header.byte_order) as usize;
184                if offset + total_size <= data.len() {
185                    *result = Some((offset, total_size));
186                }
187                break;
188            }
189        }
190    }
191
192    /// Parse EXIF data from a byte slice with an explicit byte order and offset.
193    fn read_ifd(
194        data: &[u8],
195        header: &TiffHeader,
196        offset: u32,
197        ifd_name: &str,
198        tags: &mut Vec<Tag>,
199    ) -> Result<Option<u32>> {
200        let offset = offset as usize;
201        if offset + 2 > data.len() {
202            return Err(Error::InvalidExif(format!(
203                "{} offset {} beyond data length {}",
204                ifd_name,
205                offset,
206                data.len()
207            )));
208        }
209
210        let entry_count = read_u16(data, offset, header.byte_order) as usize;
211        let entries_start = offset + 2;
212        let _entries_end = entries_start + entry_count * 12;
213
214        // Validate: at minimum, first entry must fit
215        if entries_start + 12 > data.len() && entry_count > 0 {
216            return Err(Error::InvalidExif(format!(
217                "{} entries extend beyond data (need {}, have {})",
218                ifd_name,
219                entries_start + 12,
220                data.len()
221            )));
222        }
223        // Clamp entry count if IFD extends beyond data
224        let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
225        let entries_end = entries_start + entry_count * 12;
226
227        for i in 0..entry_count {
228            let entry_offset = entries_start + i * 12;
229            let entry = parse_ifd_entry(data, entry_offset, header.byte_order);
230
231            // Check for sub-IFDs (ExifIFD, GPS, Interop)
232            match entry.tag {
233                0x8769 => {
234                    // ExifIFD
235                    let sub_offset = entry.value_offset;
236                    if (sub_offset as usize) < data.len() {
237                        let _ = Self::read_ifd(data, header, sub_offset, "ExifIFD", tags);
238                    }
239                    continue;
240                }
241                0x8825 => {
242                    // GPS IFD
243                    let sub_offset = entry.value_offset;
244                    if (sub_offset as usize) < data.len() {
245                        let _ = Self::read_ifd(data, header, sub_offset, "GPS", tags);
246                    }
247                    continue;
248                }
249                0xA005 => {
250                    // Interop IFD
251                    let sub_offset = entry.value_offset;
252                    if (sub_offset as usize) < data.len() {
253                        let _ = Self::read_ifd(data, header, sub_offset, "InteropIFD", tags);
254                    }
255                    continue;
256                }
257                // PrintIM tag: extract version from "PrintIM" + 4-byte version
258                0xC4A5 => {
259                    let total_size = match entry.data_type {
260                        1 | 2 | 6 | 7 => entry.count as usize,
261                        _ => 0,
262                    };
263                    if total_size > 11 {
264                        let off = entry.value_offset as usize;
265                        if off + 11 <= data.len() && &data[off..off+7] == b"PrintIM" {
266                            let ver = String::from_utf8_lossy(&data[off+7..off+11]).to_string();
267                            tags.push(Tag {
268                                id: TagId::Text("PrintIMVersion".into()),
269                                name: "PrintIMVersion".into(),
270                                description: "PrintIM Version".into(),
271                                group: TagGroup { family0: "PrintIM".into(), family1: "PrintIM".into(), family2: "Printing".into() },
272                                raw_value: Value::String(ver.clone()),
273                                print_value: ver,
274                                priority: 0,
275                            });
276                        }
277                    }
278                    continue; // Suppress raw PrintIM tag
279                }
280                // Suppress GPS tag 0x0006 (GPSAltitude) when value is 0/0
281                0x0006 if ifd_name == "GPS" => {
282                    if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
283                        if let Value::URational(0, 0) = val {
284                            continue;
285                        }
286                    }
287                }
288                _ => {}
289            }
290
291            if let Some(mut value) = read_ifd_value(data, &entry, header.byte_order) {
292                // GPS TimeStamp (0x0007): convert 0/0 rationals to 0/1 so it displays as "0, 0, 0"
293                // (Perl treats 0/0 as 0 for GPS time, enabling GPSDateTime composite)
294                if ifd_name == "GPS" && entry.tag == 0x0007 {
295                    if let Value::List(ref mut items) = value {
296                        for item in items.iter_mut() {
297                            if matches!(item, Value::URational(0, 0)) {
298                                *item = Value::URational(0, 1);
299                            }
300                        }
301                    }
302                }
303                let tag_info = exif_tags::lookup(ifd_name, entry.tag);
304                let (name, description, family2) = match tag_info {
305                    Some(info) => (
306                        info.name.to_string(),
307                        info.description.to_string(),
308                        info.family2.to_string(),
309                    ),
310                    None => {
311                        // Fallback to generated tags
312                        match exif_tags::lookup_generated(entry.tag) {
313                            Some((n, d)) => (n.to_string(), d.to_string(), "Other".to_string()),
314                            None => (
315                                format!("Tag0x{:04X}", entry.tag),
316                                format!("Unknown tag 0x{:04X}", entry.tag),
317                                "Other".to_string(),
318                            ),
319                        }
320                    }
321                };
322
323                let print_value =
324                    exif_tags::print_conv(ifd_name, entry.tag, &value)
325                        .or_else(|| {
326                            // Fallback to generated print conversions
327                            value.as_u64()
328                                .and_then(|v| crate::tags::print_conv_generated::print_conv_by_name(&name, v as i64))
329                                .map(|s| s.to_string())
330                        })
331                        .unwrap_or_else(|| value.to_display_string());
332
333                tags.push(Tag {
334                    id: TagId::Numeric(entry.tag),
335                    name,
336                    description,
337                    group: TagGroup {
338                        family0: "EXIF".to_string(),
339                        family1: ifd_name.to_string(),
340                        family2,
341                    },
342                    raw_value: value,
343                    print_value,
344                    priority: 0,
345                });
346            }
347        }
348
349        // Read next IFD offset
350        let next_ifd_offset = if entries_end + 4 <= data.len() {
351            read_u32(data, entries_end, header.byte_order)
352        } else { 0 };
353        if next_ifd_offset != 0 && ifd_name == "IFD0" {
354            // IFD1 = thumbnail
355            let _ = Self::read_ifd(data, header, next_ifd_offset, "IFD1", tags);
356
357            // Create ThumbnailImage tag if offset+length are present
358            let thumb_offset = tags.iter()
359                .find(|t| t.name == "ThumbnailOffset" && t.group.family1 == "IFD1")
360                .and_then(|t| t.raw_value.as_u64());
361            let thumb_length = tags.iter()
362                .find(|t| t.name == "ThumbnailLength" && t.group.family1 == "IFD1")
363                .and_then(|t| t.raw_value.as_u64());
364
365            if let (Some(off), Some(len)) = (thumb_offset, thumb_length) {
366                let off = off as usize;
367                let len = len as usize;
368                if off + len <= data.len() && len > 0 {
369                    tags.push(Tag {
370                        id: TagId::Text("ThumbnailImage".into()),
371                        name: "ThumbnailImage".into(),
372                        description: "Thumbnail Image".into(),
373                        group: TagGroup { family0: "EXIF".into(), family1: "IFD1".into(), family2: "Image".into() },
374                        raw_value: Value::Binary(data[off..off+len].to_vec()),
375                        print_value: format!("(Binary data {} bytes)", len),
376                        priority: 0,
377                    });
378                }
379            }
380        }
381
382        Ok(if next_ifd_offset != 0 {
383            Some(next_ifd_offset)
384        } else {
385            None
386        })
387    }
388}
389
390fn parse_ifd_entry(data: &[u8], offset: usize, byte_order: ByteOrderMark) -> IfdEntry {
391    let tag = read_u16(data, offset, byte_order);
392    let data_type = read_u16(data, offset + 2, byte_order);
393    let count = read_u32(data, offset + 4, byte_order);
394    let value_offset = read_u32(data, offset + 8, byte_order);
395    let mut inline_data = [0u8; 4];
396    inline_data.copy_from_slice(&data[offset + 8..offset + 12]);
397
398    IfdEntry {
399        tag,
400        data_type,
401        count,
402        value_offset,
403        inline_data,
404    }
405}
406
407fn read_ifd_value(data: &[u8], entry: &IfdEntry, byte_order: ByteOrderMark) -> Option<Value> {
408    let elem_size = type_size(entry.data_type)?;
409    let total_size = elem_size * entry.count as usize;
410
411    let value_data = if total_size <= 4 {
412        &entry.inline_data[..total_size]
413    } else {
414        let offset = entry.value_offset as usize;
415        if offset + total_size > data.len() {
416            return None;
417        }
418        &data[offset..offset + total_size]
419    };
420
421    match entry.data_type {
422        // BYTE
423        1 => {
424            if entry.count == 1 {
425                Some(Value::U8(value_data[0]))
426            } else {
427                Some(Value::List(value_data.iter().map(|&b| Value::U8(b)).collect()))
428            }
429        }
430        // ASCII
431        2 => {
432            let s = String::from_utf8_lossy(value_data);
433            Some(Value::String(s.trim_end_matches('\0').to_string()))
434        }
435        // SHORT
436        3 => {
437            if entry.count == 1 {
438                Some(Value::U16(read_u16(value_data, 0, byte_order)))
439            } else {
440                let vals: Vec<Value> = (0..entry.count as usize)
441                    .map(|i| Value::U16(read_u16(value_data, i * 2, byte_order)))
442                    .collect();
443                Some(Value::List(vals))
444            }
445        }
446        // LONG
447        4 | 13 => {
448            if entry.count == 1 {
449                Some(Value::U32(read_u32(value_data, 0, byte_order)))
450            } else {
451                let vals: Vec<Value> = (0..entry.count as usize)
452                    .map(|i| Value::U32(read_u32(value_data, i * 4, byte_order)))
453                    .collect();
454                Some(Value::List(vals))
455            }
456        }
457        // RATIONAL (unsigned)
458        5 => {
459            if entry.count == 1 {
460                let n = read_u32(value_data, 0, byte_order);
461                let d = read_u32(value_data, 4, byte_order);
462                Some(Value::URational(n, d))
463            } else {
464                let vals: Vec<Value> = (0..entry.count as usize)
465                    .map(|i| {
466                        let n = read_u32(value_data, i * 8, byte_order);
467                        let d = read_u32(value_data, i * 8 + 4, byte_order);
468                        Value::URational(n, d)
469                    })
470                    .collect();
471                Some(Value::List(vals))
472            }
473        }
474        // SBYTE
475        6 => {
476            if entry.count == 1 {
477                Some(Value::I16(value_data[0] as i8 as i16))
478            } else {
479                let vals: Vec<Value> = value_data
480                    .iter()
481                    .map(|&b| Value::I16(b as i8 as i16))
482                    .collect();
483                Some(Value::List(vals))
484            }
485        }
486        // UNDEFINED
487        7 => Some(Value::Undefined(value_data.to_vec())),
488        // SSHORT
489        8 => {
490            if entry.count == 1 {
491                Some(Value::I16(read_i16(value_data, 0, byte_order)))
492            } else {
493                let vals: Vec<Value> = (0..entry.count as usize)
494                    .map(|i| Value::I16(read_i16(value_data, i * 2, byte_order)))
495                    .collect();
496                Some(Value::List(vals))
497            }
498        }
499        // SLONG
500        9 => {
501            if entry.count == 1 {
502                Some(Value::I32(read_i32(value_data, 0, byte_order)))
503            } else {
504                let vals: Vec<Value> = (0..entry.count as usize)
505                    .map(|i| Value::I32(read_i32(value_data, i * 4, byte_order)))
506                    .collect();
507                Some(Value::List(vals))
508            }
509        }
510        // SRATIONAL
511        10 => {
512            if entry.count == 1 {
513                let n = read_i32(value_data, 0, byte_order);
514                let d = read_i32(value_data, 4, byte_order);
515                Some(Value::IRational(n, d))
516            } else {
517                let vals: Vec<Value> = (0..entry.count as usize)
518                    .map(|i| {
519                        let n = read_i32(value_data, i * 8, byte_order);
520                        let d = read_i32(value_data, i * 8 + 4, byte_order);
521                        Value::IRational(n, d)
522                    })
523                    .collect();
524                Some(Value::List(vals))
525            }
526        }
527        // FLOAT
528        11 => {
529            if entry.count == 1 {
530                let bits = read_u32(value_data, 0, byte_order);
531                Some(Value::F32(f32::from_bits(bits)))
532            } else {
533                let vals: Vec<Value> = (0..entry.count as usize)
534                    .map(|i| {
535                        let bits = read_u32(value_data, i * 4, byte_order);
536                        Value::F32(f32::from_bits(bits))
537                    })
538                    .collect();
539                Some(Value::List(vals))
540            }
541        }
542        // DOUBLE
543        12 => {
544            if entry.count == 1 {
545                let bits = read_u64(value_data, 0, byte_order);
546                Some(Value::F64(f64::from_bits(bits)))
547            } else {
548                let vals: Vec<Value> = (0..entry.count as usize)
549                    .map(|i| {
550                        let bits = read_u64(value_data, i * 8, byte_order);
551                        Value::F64(f64::from_bits(bits))
552                    })
553                    .collect();
554                Some(Value::List(vals))
555            }
556        }
557        _ => None,
558    }
559}
560
561// Byte-order-aware read helpers
562fn read_u16(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
563    match bo {
564        ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[offset..]),
565        ByteOrderMark::BigEndian => BigEndian::read_u16(&data[offset..]),
566    }
567}
568
569fn read_u32(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
570    match bo {
571        ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[offset..]),
572        ByteOrderMark::BigEndian => BigEndian::read_u32(&data[offset..]),
573    }
574}
575
576fn read_u64(data: &[u8], offset: usize, bo: ByteOrderMark) -> u64 {
577    match bo {
578        ByteOrderMark::LittleEndian => LittleEndian::read_u64(&data[offset..]),
579        ByteOrderMark::BigEndian => BigEndian::read_u64(&data[offset..]),
580    }
581}
582
583fn read_i16(data: &[u8], offset: usize, bo: ByteOrderMark) -> i16 {
584    match bo {
585        ByteOrderMark::LittleEndian => LittleEndian::read_i16(&data[offset..]),
586        ByteOrderMark::BigEndian => BigEndian::read_i16(&data[offset..]),
587    }
588}
589
590fn read_i32(data: &[u8], offset: usize, bo: ByteOrderMark) -> i32 {
591    match bo {
592        ByteOrderMark::LittleEndian => LittleEndian::read_i32(&data[offset..]),
593        ByteOrderMark::BigEndian => BigEndian::read_i32(&data[offset..]),
594    }
595}