Skip to main content

smugmug_cli/raw/
exif.rs

1//! Reading a RAW file's EXIF so it can be written into the JPEG rendered
2//! from it, which usually has none of its own. Writing is `little_exif`'s
3//! job; reading is done here because RAW containers (CR3's boxes, the
4//! Olympus and Panasonic TIFF variants) are outside what it parses.
5//!
6//! Only plain values are copied: the camera/date/exposure tags of IFD0, the
7//! EXIF sub-IFD minus the MakerNote (proprietary, and full of offsets that
8//! would dangle once moved), and the GPS sub-IFD. IFD0's Orientation is kept
9//! so viewers rotate the (unrotated) preview the way the camera was held.
10
11use little_exif::endian::Endian;
12use little_exif::exif_tag::ExifTag;
13use little_exif::exif_tag_format::ExifTagFormat;
14use little_exif::filetype::FileExtension;
15use little_exif::ifd::ExifTagGroup;
16use little_exif::metadata::Metadata;
17
18/// One IFD entry with its value bytes, always in little-endian order.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Entry {
21    pub tag: u16,
22    pub typ: u16,
23    pub count: u32,
24    pub value: Vec<u8>,
25}
26
27/// The tags worth carrying from a RAW file to its rendered JPEG.
28#[derive(Debug, Default, Clone, PartialEq, Eq)]
29pub struct ExifData {
30    pub ifd0: Vec<Entry>,
31    pub exif: Vec<Entry>,
32    pub gps: Vec<Entry>,
33}
34
35const TAG_EXIF_IFD: u16 = 0x8769;
36const TAG_GPS_IFD: u16 = 0x8825;
37const TAG_INTEROP_IFD: u16 = 0xA005;
38const TAG_MAKER_NOTE: u16 = 0x927C;
39const TAG_PIXEL_X_DIMENSION: u16 = 0xA002;
40const TAG_PIXEL_Y_DIMENSION: u16 = 0xA003;
41const TAG_ORIENTATION: u16 = 0x0112;
42
43/// IFD0 tags copied to the JPEG. The rest of a RAW's IFD0 describes its own
44/// image data (strips, compression, sensor layout) and would be wrong there.
45const IFD0_TAGS: &[u16] = &[
46    0x010E, // ImageDescription
47    0x010F, // Make
48    0x0110, // Model
49    TAG_ORIENTATION,
50    0x0131, // Software
51    0x0132, // DateTime
52    0x013B, // Artist
53    0x8298, // Copyright
54];
55
56/// Values bigger than this (long UserComments and the like) are dropped so
57/// the segment stays under JPEG's 64 KB segment limit.
58const MAX_VALUE_LEN: usize = 4096;
59
60/// Size in bytes of one element of an IFD type, and the size of the units
61/// whose byte order must be swapped (rationals are pairs of 4-byte ints).
62fn type_sizes(typ: u16) -> Option<(usize, usize)> {
63    Some(match typ {
64        1 | 2 | 6 | 7 => (1, 1),   // BYTE, ASCII, SBYTE, UNDEFINED
65        3 | 8 => (2, 2),           // SHORT, SSHORT
66        4 | 9 | 11 | 13 => (4, 4), // LONG, SLONG, FLOAT, IFD
67        5 | 10 => (8, 4),          // RATIONAL, SRATIONAL
68        12 => (8, 8),              // DOUBLE
69        _ => return None,
70    })
71}
72
73/// A TIFF structure (header plus IFDs) inside a byte slice.
74struct Tiff<'a> {
75    data: &'a [u8],
76    little_endian: bool,
77}
78
79impl<'a> Tiff<'a> {
80    /// Accepts standard TIFF headers and the variants RAW formats use
81    /// (Olympus "IIRO"/"IIRS"/"MMOR", Panasonic "IIU\0"). Returns the TIFF
82    /// and the offset of its first IFD.
83    fn parse(data: &'a [u8]) -> Option<(Self, u32)> {
84        let little_endian = match data.get(0..2)? {
85            b"II" => true,
86            b"MM" => false,
87            _ => return None,
88        };
89        let tiff = Tiff {
90            data,
91            little_endian,
92        };
93        let magic = tiff.u16(2)?;
94        if !matches!(magic, 0x2A | 0x4F52 | 0x5352 | 0x55) {
95            return None;
96        }
97        let first_ifd = tiff.u32(4)?;
98        Some((tiff, first_ifd))
99    }
100
101    fn u16(&self, at: usize) -> Option<u16> {
102        let b = self.data.get(at..at + 2)?;
103        Some(if self.little_endian {
104            u16::from_le_bytes([b[0], b[1]])
105        } else {
106            u16::from_be_bytes([b[0], b[1]])
107        })
108    }
109
110    fn u32(&self, at: usize) -> Option<u32> {
111        let b = self.data.get(at..at + 4)?;
112        Some(if self.little_endian {
113            u32::from_le_bytes([b[0], b[1], b[2], b[3]])
114        } else {
115            u32::from_be_bytes([b[0], b[1], b[2], b[3]])
116        })
117    }
118
119    /// The entries of the IFD at `offset`. Entries of unknown types or whose
120    /// values fall outside the data are skipped.
121    fn ifd(&self, offset: u32) -> Option<Vec<Entry>> {
122        let offset = offset as usize;
123        let count = self.u16(offset)? as usize;
124        let mut entries = Vec::with_capacity(count);
125        for i in 0..count {
126            let at = offset + 2 + i * 12;
127            let tag = self.u16(at)?;
128            let typ = self.u16(at + 2)?;
129            let n = self.u32(at + 4)?;
130            let Some((elem, unit)) = type_sizes(typ) else {
131                continue;
132            };
133            let Some(len) = (n as usize).checked_mul(elem) else {
134                continue;
135            };
136            let value_at = if len <= 4 {
137                at + 8
138            } else {
139                self.u32(at + 8)? as usize
140            };
141            let Some(bytes) = value_at
142                .checked_add(len)
143                .and_then(|end| self.data.get(value_at..end))
144            else {
145                continue;
146            };
147            let mut value = bytes.to_vec();
148            if !self.little_endian && unit > 1 {
149                for chunk in value.chunks_mut(unit) {
150                    chunk.reverse();
151                }
152            }
153            entries.push(Entry {
154                tag,
155                typ,
156                count: n,
157                value,
158            });
159        }
160        Some(entries)
161    }
162
163    /// The offset stored in a pointer tag (e.g. the EXIF sub-IFD) of `ifd`.
164    fn pointer(ifd: &[Entry], tag: u16) -> Option<u32> {
165        let entry = ifd.iter().find(|e| e.tag == tag)?;
166        let b = entry.value.get(0..4)?;
167        Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
168    }
169}
170
171fn copyable(entry: &Entry) -> bool {
172    entry.value.len() <= MAX_VALUE_LEN
173}
174
175/// Read EXIF from a TIFF-based RAW file (CR2, NEF, ARW, DNG, ORF, RW2, PEF,
176/// ...). `None` if `data` doesn't start with a TIFF header.
177pub fn read_tiff_raw(data: &[u8]) -> Option<ExifData> {
178    let (tiff, ifd0_offset) = Tiff::parse(data)?;
179    let ifd0 = tiff.ifd(ifd0_offset)?;
180
181    let exif = Tiff::pointer(&ifd0, TAG_EXIF_IFD)
182        .and_then(|o| tiff.ifd(o))
183        .unwrap_or_default();
184    let gps = Tiff::pointer(&ifd0, TAG_GPS_IFD)
185        .and_then(|o| tiff.ifd(o))
186        .unwrap_or_default();
187
188    Some(ExifData {
189        ifd0: ifd0
190            .into_iter()
191            .filter(|e| IFD0_TAGS.contains(&e.tag) && copyable(e))
192            .collect(),
193        exif: exif.into_iter().filter(keep_exif_entry).collect(),
194        gps: gps.into_iter().filter(copyable).collect(),
195    })
196}
197
198fn keep_exif_entry(entry: &Entry) -> bool {
199    !matches!(
200        entry.tag,
201        TAG_MAKER_NOTE
202            | TAG_INTEROP_IFD
203            | TAG_EXIF_IFD
204            | TAG_GPS_IFD
205            // Describe the RAW's full size, not the preview's
206            | TAG_PIXEL_X_DIMENSION
207            | TAG_PIXEL_Y_DIMENSION
208    ) && entry.typ != 13
209        && copyable(entry)
210}
211
212/// Read EXIF from a Canon CR3 file. CR3 is an ISO BMFF container whose
213/// metadata sits in boxes that each hold a complete TIFF structure: CMT1
214/// (IFD0), CMT2 (EXIF sub-IFD) and CMT4 (GPS). They live in the movie
215/// header near the start of the file.
216pub fn read_cr3(data: &[u8]) -> Option<ExifData> {
217    if data.get(4..12)? != b"ftypcrx " {
218        return None;
219    }
220    let search = &data[..data.len().min(4 * 1024 * 1024)];
221    let ifd_of = |name: &[u8; 4]| -> Option<Vec<Entry>> {
222        let at = search.windows(4).position(|w| w == name)?;
223        let size = u32::from_be_bytes(search.get(at.checked_sub(4)?..at)?.try_into().ok()?);
224        let payload = search.get(at + 4..(at - 4).checked_add(size as usize)?)?;
225        let (tiff, offset) = Tiff::parse(payload)?;
226        tiff.ifd(offset)
227    };
228
229    let ifd0 = ifd_of(b"CMT1")?;
230    Some(ExifData {
231        ifd0: ifd0
232            .into_iter()
233            .filter(|e| IFD0_TAGS.contains(&e.tag) && copyable(e))
234            .collect(),
235        exif: ifd_of(b"CMT2")
236            .unwrap_or_default()
237            .into_iter()
238            .filter(keep_exif_entry)
239            .collect(),
240        gps: ifd_of(b"CMT4")
241            .unwrap_or_default()
242            .into_iter()
243            .filter(copyable)
244            .collect(),
245    })
246}
247
248/// Read whatever EXIF a RAW file's container gives access to.
249pub fn read_raw(data: &[u8]) -> Option<ExifData> {
250    read_tiff_raw(data).or_else(|| read_cr3(data))
251}
252
253impl ExifData {
254    pub fn is_empty(&self) -> bool {
255        self.ifd0.is_empty() && self.exif.is_empty() && self.gps.is_empty()
256    }
257
258    /// Write these tags into `jpeg` as its EXIF segment, replacing any it
259    /// has. Tags whose type doesn't match what EXIF defines for them are
260    /// left out.
261    pub fn write_into_jpeg(&self, jpeg: &mut Vec<u8>) -> std::io::Result<()> {
262        let mut metadata = Metadata::new();
263        let groups = [
264            (&self.ifd0, ExifTagGroup::GENERIC),
265            (&self.exif, ExifTagGroup::EXIF),
266            (&self.gps, ExifTagGroup::GPS),
267        ];
268        for (entries, group) in groups {
269            for e in entries {
270                let Some(format) = ExifTagFormat::from_u16(e.typ) else {
271                    continue;
272                };
273                if let Ok(tag) =
274                    ExifTag::from_u16_with_data(e.tag, &format, &e.value, &Endian::Little, &group)
275                {
276                    metadata.set_tag(tag);
277                }
278            }
279        }
280        metadata.write_to_vec(jpeg, FileExtension::JPEG)
281    }
282}
283
284#[cfg(test)]
285pub(crate) mod tests {
286    use super::*;
287
288    impl ExifData {
289        /// The Orientation value (1-8), if present.
290        pub fn orientation(&self) -> Option<u16> {
291            let e = self.ifd0.iter().find(|e| e.tag == TAG_ORIENTATION)?;
292            (e.typ == 3 && e.value.len() >= 2).then(|| u16::from_le_bytes([e.value[0], e.value[1]]))
293        }
294    }
295
296    /// Builds a big-endian TIFF with an IFD0 (tags given), an EXIF sub-IFD
297    /// and a GPS sub-IFD, like a RAW file's header.
298    pub fn fake_tiff_raw_be() -> Vec<u8> {
299        // Layout: header (8) | IFD0 at 8 | EXIF IFD | GPS IFD | value data
300        let make = b"Canon\0";
301        let date = b"2024:06:01 12:34:56\0";
302        let ifd0_entries = 6; // Make, Orientation, StripOffsets, DateTime, EXIF ptr, GPS ptr
303        let ifd0_at = 8;
304        let exif_at = ifd0_at + 2 + ifd0_entries * 12 + 4;
305        let exif_entries = 3; // ExposureTime, DateTimeOriginal, MakerNote
306        let gps_at = exif_at + 2 + exif_entries * 12 + 4;
307        let gps_entries = 1; // GPSLatitudeRef
308        let data_at = gps_at + 2 + gps_entries * 12 + 4;
309
310        let mut t = b"MM\x00\x2A".to_vec();
311        t.extend((ifd0_at as u32).to_be_bytes());
312
313        let mut data: Vec<u8> = Vec::new();
314        let mut put = |bytes: &[u8]| -> u32 {
315            let at = data_at + data.len();
316            data.extend(bytes);
317            at as u32
318        };
319        let make_at = put(make);
320        let date_at = put(date);
321        let exposure_at = put(&[0, 0, 0, 1, 0, 0, 0, 125]);
322        let dto_at = put(date);
323        let maker_note_at = put(&[0xAB; 64]);
324
325        let entry = |t: &mut Vec<u8>, tag: u16, typ: u16, count: u32, value: [u8; 4]| {
326            t.extend(tag.to_be_bytes());
327            t.extend(typ.to_be_bytes());
328            t.extend(count.to_be_bytes());
329            t.extend(value);
330        };
331
332        t.extend((ifd0_entries as u16).to_be_bytes());
333        entry(&mut t, 0x010F, 2, make.len() as u32, make_at.to_be_bytes());
334        entry(&mut t, 0x0111, 4, 1, 12345u32.to_be_bytes()); // StripOffsets
335        entry(&mut t, TAG_ORIENTATION, 3, 1, [0, 6, 0, 0]);
336        entry(&mut t, 0x0132, 2, date.len() as u32, date_at.to_be_bytes());
337        entry(&mut t, TAG_EXIF_IFD, 4, 1, (exif_at as u32).to_be_bytes());
338        entry(&mut t, TAG_GPS_IFD, 4, 1, (gps_at as u32).to_be_bytes());
339        t.extend(0u32.to_be_bytes());
340
341        t.extend((exif_entries as u16).to_be_bytes());
342        entry(&mut t, 0x829A, 5, 1, exposure_at.to_be_bytes());
343        entry(&mut t, 0x9003, 2, date.len() as u32, dto_at.to_be_bytes());
344        entry(&mut t, TAG_MAKER_NOTE, 7, 64, maker_note_at.to_be_bytes());
345        t.extend(0u32.to_be_bytes());
346
347        t.extend((gps_entries as u16).to_be_bytes());
348        entry(&mut t, 0x0001, 2, 2, *b"N\0\0\0");
349        t.extend(0u32.to_be_bytes());
350
351        assert_eq!(t.len(), data_at);
352        t.extend(data);
353        t
354    }
355
356    #[test]
357    fn reads_tiff_raw_and_filters_tags() {
358        let exif = read_raw(&fake_tiff_raw_be()).unwrap();
359
360        let ifd0_tags: Vec<u16> = exif.ifd0.iter().map(|e| e.tag).collect();
361        assert_eq!(ifd0_tags, vec![0x010F, TAG_ORIENTATION, 0x0132]);
362        assert_eq!(exif.orientation(), Some(6));
363        assert_eq!(exif.ifd0[0].value, b"Canon\0");
364
365        let exif_tags: Vec<u16> = exif.exif.iter().map(|e| e.tag).collect();
366        assert_eq!(exif_tags, vec![0x829A, 0x9003]);
367        // Rational converted to little-endian, one u32 at a time
368        assert_eq!(exif.exif[0].value, vec![1, 0, 0, 0, 125, 0, 0, 0]);
369
370        assert_eq!(exif.gps.len(), 1);
371    }
372
373    /// The TIFF inside `jpeg`'s EXIF segment, found by walking its markers.
374    pub fn exif_tiff(jpeg: &[u8]) -> Option<&[u8]> {
375        let mut p = 2;
376        while jpeg.get(p) == Some(&0xFF) && jpeg.get(p + 1) != Some(&0xDA) {
377            let len = u16::from_be_bytes([*jpeg.get(p + 2)?, *jpeg.get(p + 3)?]) as usize;
378            let segment = jpeg.get(p + 4..p + 2 + len)?;
379            if jpeg[p + 1] == 0xE1 && segment.starts_with(b"Exif\0\0") {
380                return Some(&segment[6..]);
381            }
382            p += 2 + len;
383        }
384        None
385    }
386
387    #[test]
388    fn written_exif_reads_back() {
389        let exif = read_raw(&fake_tiff_raw_be()).unwrap();
390        let mut jpeg = crate::raw::jpeg::tests::fake_jpeg(0xC0, 1920, 1280, None);
391        exif.write_into_jpeg(&mut jpeg).unwrap();
392
393        // Still one well-formed JPEG, now with EXIF
394        let found = crate::raw::jpeg::find_jpegs(&jpeg);
395        assert_eq!(found.len(), 1);
396        assert!(found[0].has_exif);
397
398        // Our own reader sees the same tags in what little_exif wrote.
399        let reread = read_tiff_raw(exif_tiff(&jpeg).unwrap()).unwrap();
400        assert_eq!(reread, exif);
401    }
402
403    #[test]
404    fn reads_cr3_metadata_boxes() {
405        // A TIFF holding just IFD0 with Model and Orientation, as in CMT1.
406        let mut cmt1 = b"II\x2A\x00\x08\x00\x00\x00".to_vec();
407        cmt1.extend(2u16.to_le_bytes());
408        cmt1.extend(0x0110u16.to_le_bytes());
409        cmt1.extend(2u16.to_le_bytes());
410        cmt1.extend(4u32.to_le_bytes());
411        cmt1.extend(*b"R5\0\0");
412        cmt1.extend(TAG_ORIENTATION.to_le_bytes());
413        cmt1.extend(3u16.to_le_bytes());
414        cmt1.extend(1u32.to_le_bytes());
415        cmt1.extend([8, 0, 0, 0]);
416        cmt1.extend(0u32.to_le_bytes());
417
418        let mut data = vec![0, 0, 0, 0x18];
419        data.extend(b"ftypcrx ");
420        data.extend([0u8; 12]);
421        data.extend(((cmt1.len() + 8) as u32).to_be_bytes());
422        data.extend(b"CMT1");
423        data.extend(&cmt1);
424
425        let exif = read_raw(&data).unwrap();
426        assert_eq!(exif.ifd0.len(), 2);
427        assert_eq!(exif.orientation(), Some(8));
428        assert!(exif.exif.is_empty());
429    }
430}