Skip to main content

falcon_library/
exif.rs

1//! Camera metadata stored at import as `rawMetas`, in the shape Eagle writes
2//! for RAW photos (see the Import chapter of the docs).
3//!
4//! Eagle gets these values from dcraw's identify output; falcon reads the
5//! same facts from the file's EXIF and formats them the way dcraw prints them.
6
7use std::path::Path;
8
9use chrono::{Local, NaiveDateTime, TimeZone};
10use ::exif::{In, Tag, Value};
11use serde_json::{Map, Value as Json, json};
12
13/// Extensions Eagle treats as RAW photos and analyses with dcraw.
14pub const RAW_EXTENSIONS: [&str; 18] =
15    ["3fr", "arw", "orf", "mrw", "nef", "nrw", "raw", "cr2", "crw", "dng", "erf", "pef", "raf", "rw2", "sr2", "srw", "x3f", "cr3"];
16
17/// Other photo formats whose EXIF falcon can read.
18pub const EXIF_EXTENSIONS: [&str; 12] = ["jpg", "jpeg", "jfif", "jpe", "insp", "tif", "tiff", "heic", "heif", "hif", "png", "webp"];
19
20pub fn is_raw(ext: &str) -> bool {
21    RAW_EXTENSIONS.contains(&ext)
22}
23
24/// Camera facts read from EXIF.
25#[derive(Debug, Clone, Default, PartialEq)]
26pub struct CameraMeta {
27    /// Date taken, ms since the epoch, read as local time.
28    pub timestamp: Option<i64>,
29    /// Make and model, as dcraw names them.
30    pub camera: Option<String>,
31    pub iso: Option<u32>,
32    /// Millimetres.
33    pub focal_length: Option<f64>,
34    /// Seconds.
35    pub shutter: Option<f64>,
36    /// The f-number.
37    pub aperture: Option<f64>,
38}
39
40impl CameraMeta {
41    pub fn is_empty(&self) -> bool {
42        *self == CameraMeta::default()
43    }
44
45    /// The `rawMetas` object: `timestamp`, `camera`, `isoSpeed`,
46    /// `focalLength`, `shutter`, `aperture`, each only when known.
47    pub fn to_raw_metas(&self) -> Map<String, Json> {
48        let mut m = Map::new();
49        if let Some(t) = self.timestamp {
50            m.insert("timestamp".into(), json!(t));
51        }
52        if let Some(c) = &self.camera {
53            m.insert("camera".into(), json!(c));
54        }
55        if let Some(i) = self.iso {
56            m.insert("isoSpeed".into(), json!(i));
57        }
58        if let Some(f) = self.focal_length {
59            m.insert("focalLength".into(), json!(format!("{f:.1} mm")));
60        }
61        if let Some(s) = self.shutter {
62            m.insert("shutter".into(), json!(shutter_text(s)));
63        }
64        if let Some(a) = self.aperture {
65            m.insert("aperture".into(), json!(format!("f/{a:.1}")));
66        }
67        m
68    }
69}
70
71/// dcraw's exposure text: `1/320.0 sec` under a second, else `2.0 sec`.
72pub fn shutter_text(s: f64) -> String {
73    if s > 0.0 && s < 1.0 { format!("1/{:.1} sec", 1.0 / s) } else { format!("{s:.1} sec") }
74}
75
76/// Maker names dcraw shortens a Make tag to when the tag contains them.
77const MAKERS: [&str; 24] = [
78    "AgfaPhoto", "Canon", "Casio", "Epson", "Fujifilm", "Hasselblad", "Kodak", "Konica", "Leaf", "Leica", "Mamiya", "Minolta",
79    "Motorola", "Nikon", "Nokia", "Olympus", "Panasonic", "Pentax", "Phase One", "Ricoh", "Samsung", "Sigma", "Sinar", "Sony",
80];
81
82/// `Make Model` the way dcraw prints it: the maker shortened to its usual
83/// name and repeated maker words removed from the model.
84pub fn camera_name(make: &str, model: &str) -> String {
85    let make = make.trim();
86    let lower = make.to_lowercase();
87    let make = MAKERS.iter().find(|m| lower.contains(&m.to_lowercase())).map_or(make.to_string(), |m| m.to_string());
88    let mut model = model.trim();
89    if let Some(rest) = model.get(make.len()..).filter(|_| model.to_lowercase().starts_with(&make.to_lowercase()))
90        && rest.starts_with(' ')
91    {
92        model = rest.trim_start();
93    }
94    match (make.is_empty(), model.is_empty()) {
95        (true, _) => model.to_string(),
96        (_, true) => make,
97        _ => format!("{make} {model}"),
98    }
99}
100
101/// The first primary-image field with tag number `number` in any of the
102/// blocks. Matching on the number alone lets a block that holds EXIF tags in
103/// its first IFD (CR3's `CMT2`) count like a proper EXIF sub-IFD.
104fn field(blocks: &[::exif::Exif], number: u16) -> Option<&::exif::Field> {
105    blocks.iter().flat_map(|e| e.fields()).find(|f| f.ifd_num == In::PRIMARY && f.tag.number() == number)
106}
107
108fn text(blocks: &[::exif::Exif], tag: Tag) -> Option<String> {
109    match &field(blocks, tag.number())?.value {
110        Value::Ascii(v) => {
111            let s = String::from_utf8_lossy(v.first()?).trim_matches(char::from(0)).trim().to_string();
112            (!s.is_empty()).then_some(s)
113        }
114        _ => None,
115    }
116}
117
118fn number(blocks: &[::exif::Exif], tag: Tag) -> Option<f64> {
119    match &field(blocks, tag.number())?.value {
120        Value::Rational(v) => v.first().filter(|r| r.denom != 0).map(|r| r.to_f64()),
121        Value::SRational(v) => v.first().filter(|r| r.denom != 0).map(|r| r.to_f64()),
122        v => v.get_uint(0).map(f64::from),
123    }
124}
125
126/// The EXIF blocks in a photo file. TIFF-based RAW files are read
127/// directly; ORF and RW2 are TIFF with their own magic number in place of
128/// 42; RAF carries a JPEG preview whose EXIF holds the camera; CR3 is an
129/// ISO media file whose Canon box holds TIFF blocks `CMT1` (the main IFD)
130/// and `CMT2` (the EXIF IFD). Anything else goes to the EXIF reader's own
131/// container detection (JPEG, HEIF, PNG, WebP).
132fn exif_blocks(data: &[u8]) -> Vec<::exif::Exif> {
133    let reader = ::exif::Reader::new();
134    let tiff = |bytes: Vec<u8>| reader.read_raw(bytes).ok();
135    match data.get(..4).unwrap_or_default() {
136        b"II*\0" | b"MM\0*" => tiff(data.to_vec()).into_iter().collect(),
137        // Olympus ORF ("IIRO", "IIRS", "MMOR") and Panasonic RW2 ("IIU\0").
138        b"IIRO" | b"IIRS" | b"IIU\0" | b"MMOR" => {
139            let mut bytes = data.to_vec();
140            let magic = if bytes[0] == b'I' { b"*\0" } else { b"\0*" };
141            bytes[2..4].copy_from_slice(magic);
142            tiff(bytes).into_iter().collect()
143        }
144        _ if data.starts_with(b"FUJIFILMCCD-RAW") => {
145            let be32 = |o: usize| data.get(o..o + 4).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize);
146            let jpeg = be32(84).zip(be32(88)).and_then(|(at, len)| data.get(at..at.checked_add(len)?));
147            jpeg.and_then(|j| reader.read_from_container(&mut std::io::Cursor::new(j)).ok()).into_iter().collect()
148        }
149        b"\0MRM" => mrw_ttw(data).and_then(|t| tiff(t.to_vec())).into_iter().collect(),
150        b"FOVb" => x3f_preview(data).and_then(|j| reader.read_from_container(&mut std::io::Cursor::new(j)).ok()).into_iter().collect(),
151        _ if data.get(4..12) == Some(b"ftypcrx ") => cr3_tiff_blocks(data).into_iter().filter_map(|b| tiff(b.to_vec())).collect(),
152        _ => reader.read_from_container(&mut std::io::Cursor::new(data)).ok().into_iter().collect(),
153    }
154}
155
156/// ISO media boxes in `data`: (type, payload).
157fn boxes(data: &[u8]) -> Vec<(&[u8], &[u8])> {
158    let mut out = Vec::new();
159    let mut at = 0usize;
160    while at + 8 <= data.len() {
161        let size = u32::from_be_bytes([data[at], data[at + 1], data[at + 2], data[at + 3]]) as u64;
162        let kind = &data[at + 4..at + 8];
163        let (header, size) = match size {
164            0 => (8, (data.len() - at) as u64),
165            1 => match data.get(at + 8..at + 16) {
166                Some(b) => (16, u64::from_be_bytes(b.try_into().expect("8 bytes"))),
167                None => break,
168            },
169            n => (8, n),
170        };
171        let Some(end) = usize::try_from(size).ok().and_then(|s| at.checked_add(s)).filter(|&e| e <= data.len() && size >= header) else { break };
172        out.push((kind, &data[at + header as usize..end]));
173        at = end;
174    }
175    out
176}
177
178fn le32(data: &[u8], at: usize) -> Option<usize> {
179    data.get(at..at.checked_add(4)?).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize)
180}
181
182fn be32(data: &[u8], at: usize) -> Option<usize> {
183    data.get(at..at.checked_add(4)?).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize)
184}
185
186/// The TIFF block (`\0TTW`) of a Minolta MRW file. The file starts with
187/// `\0MRM` and the big-endian offset of the image data (counted from byte
188/// 8); before it come blocks of a 4-byte tag and a big-endian length
189/// (`\0PRD`, `\0TTW`, `\0WBG`, `\0RIF`). The TTW payload is a complete TIFF.
190fn mrw_ttw(data: &[u8]) -> Option<&[u8]> {
191    let end = be32(data, 4)?.checked_add(8)?.min(data.len());
192    let mut at = 8usize;
193    // A handful of blocks exist; the cap stops a corrupt chain of tiny ones.
194    for _ in 0..32 {
195        let tag = data.get(at..at.checked_add(4)?)?;
196        let len = be32(data, at + 4)?;
197        let body = at.checked_add(8)?;
198        let next = body.checked_add(len)?;
199        if next > end {
200            return None;
201        }
202        if tag == b"\0TTW" {
203            return data.get(body..next);
204        }
205        at = next;
206    }
207    None
208}
209
210/// The JPEG preview of a Sigma/Foveon X3F file. The last 4 bytes (LE) point
211/// at the `SECd` directory: tag, version, entry count, then entries of
212/// (offset, length, 4-char type). An `IMAG` or `IMA2` section has a 28-byte
213/// header (`SECi`, version, type, format, columns, rows, row size) before its
214/// data; any such section whose data starts with a JPEG signature is taken
215/// (the format code is not trusted, as it differs between camera
216/// generations). The header's own rotation field (offset 36) is left out.
217fn x3f_preview(data: &[u8]) -> Option<&[u8]> {
218    let dir = le32(data, data.len().checked_sub(4)?)?;
219    if data.get(dir..dir.checked_add(4)?)? != b"SECd" {
220        return None;
221    }
222    let count = le32(data, dir + 8)?.min(256);
223    for i in 0..count {
224        let e = dir.checked_add(12)?.checked_add(i * 12)?;
225        let (offset, length) = (le32(data, e)?, le32(data, e + 4)?);
226        let kind = data.get(e + 8..e + 12)?;
227        if kind != b"IMAG" && kind != b"IMA2" || length < 28 {
228            continue;
229        }
230        let section = data.get(offset..offset.checked_add(length)?)?;
231        if section.starts_with(b"SECi") && section[28..].starts_with(&[0xFF, 0xD8, 0xFF]) {
232            return Some(&section[28..]);
233        }
234    }
235    None
236}
237
238/// Orientation of a Canon CRW (CIFF) file. Header: `II`, LE header length,
239/// `HEAPCCDR`; the heap runs from the header length to the end of the file.
240/// The last 4 bytes of a heap give the offset (from the heap start) of its
241/// record table: u16 count, then 10-byte entries of u16 tag, u32 size, u32
242/// offset. Tag bits 0x3800 are the data type; 0x2800 and 0x3000 mean the
243/// data is itself a heap, so the search recurses (to a fixed depth). Bits
244/// 0x4000 mean the data sits inside the entry and has no heap location.
245/// Record 0x1810 (ImageInfo) holds width, height, aspect ratio (f32) and a
246/// rotation in degrees at byte 12, the clockwise turn needed to display the
247/// image upright, so 90 is EXIF 6 and 270 is EXIF 8 (as ExifTool reports it).
248fn crw_orientation(data: &[u8]) -> Option<u32> {
249    if data.get(..2)? != b"II" || data.get(6..14)? != b"HEAPCCDR" {
250        return None;
251    }
252    let start = le32(data, 2)?;
253    let info = ciff_image_info(data.get(start..)?, 0, &mut 0)?;
254    let angle = i32::from_le_bytes(info.get(12..16)?.try_into().ok()?);
255    match angle.rem_euclid(360) {
256        0 => Some(1),
257        90 => Some(6),
258        180 => Some(3),
259        270 => Some(8),
260        _ => None,
261    }
262}
263
264/// The size a Canon CRW states in its CIFF `ImageInfo` record, unturned.
265fn crw_size(data: &[u8]) -> Option<(u32, u32)> {
266    if data.get(..2)? != b"II" || data.get(6..14)? != b"HEAPCCDR" {
267        return None;
268    }
269    let info = ciff_image_info(data.get(le32(data, 2)?..)?, 0, &mut 0)?;
270    let (w, h) = (le32(info, 0)? as u32, le32(info, 4)? as u32);
271    (w > 0 && h > 0).then_some((w, h))
272}
273
274/// The `ImageInfo` record (0x1810) of a CIFF heap: width and height as u32,
275/// then the aspect ratio and the rotation.
276fn ciff_image_info<'a>(heap: &'a [u8], depth: u32, seen: &mut u32) -> Option<&'a [u8]> {
277    if depth > 6 {
278        return None;
279    }
280    let table = le32(heap, heap.len().checked_sub(4)?)?;
281    let count = heap.get(table..table.checked_add(2)?).map(|b| u16::from_le_bytes([b[0], b[1]]) as usize)?;
282    for i in 0..count {
283        *seen += 1;
284        if *seen > 512 {
285            return None;
286        }
287        let e = table.checked_add(2)?.checked_add(i * 10)?;
288        let tag = heap.get(e..e + 2).map(|b| u16::from_le_bytes([b[0], b[1]]))?;
289        if tag & 0xC000 != 0 {
290            continue;
291        }
292        let (size, offset) = (le32(heap, e + 2)?, le32(heap, e + 6)?);
293        let body = heap.get(offset..offset.checked_add(size)?)?;
294        match tag & 0x3800 {
295            0x2800 | 0x3000 => {
296                if let Some(a) = ciff_image_info(body, depth + 1, seen) {
297                    return Some(a);
298                }
299            }
300            _ if tag == 0x1810 && body.len() >= 16 => return Some(body),
301            _ => {}
302        }
303    }
304    None
305}
306
307/// Canon's metadata box inside CR3's `moov`.
308const CANON_UUID: [u8; 16] = [0x85, 0xC0, 0xB6, 0x87, 0x82, 0x0F, 0x11, 0xE0, 0x81, 0x11, 0xF4, 0xCE, 0x46, 0x2B, 0x6A, 0x48];
309
310/// The `CMT1` and `CMT2` TIFF blocks of a CR3 file.
311fn cr3_tiff_blocks(data: &[u8]) -> Vec<&[u8]> {
312    let Some((_, moov)) = boxes(data).into_iter().find(|(k, _)| *k == b"moov") else { return Vec::new() };
313    let Some((_, canon)) = boxes(moov).into_iter().find(|(k, p)| *k == b"uuid" && p.starts_with(&CANON_UUID)) else { return Vec::new() };
314    boxes(&canon[16..]).into_iter().filter(|(k, _)| *k == b"CMT1" || *k == b"CMT2").map(|(_, p)| p).collect()
315}
316
317/// Reads camera facts from a photo's EXIF; `None` when it has none.
318pub fn read(path: &Path) -> Option<CameraMeta> {
319    let data = std::fs::read(path).ok()?;
320    from_blocks(&exif_blocks(&data))
321}
322
323/// The EXIF orientation (1-8) of a photo file's main image, read from the
324/// same places as [`read`]: the TIFF IFDs of TIFF-based RAW files, the EXIF
325/// of the JPEG preview inside RAF, the `CMT1` block of CR3, the `\0TTW` TIFF
326/// of MRW, the JPEG preview of X3F and the CIFF `ImageInfo` rotation of CRW.
327pub fn orientation(data: &[u8]) -> Option<u32> {
328    let from_exif = field(&exif_blocks(data), Tag::Orientation.number()).and_then(|f| f.value.get_uint(0)).filter(|v| (1..=8).contains(v));
329    from_exif.or_else(|| crw_orientation(data))
330}
331
332/// The pixel sizes a photo file states for itself: every width and height
333/// pair (`PixelXDimension`/`PixelYDimension` in the EXIF IFD, `ImageWidth`/
334/// `ImageLength` in any IFD the reader walks), as stored, unturned. Thumbnail
335/// IFDs are in the list too, so callers pick by size.
336pub fn stated_sizes(data: &[u8]) -> Vec<(u32, u32)> {
337    let mut out = Vec::new();
338    if let Some((w, h)) = crw_size(data) {
339        out.push((w, h));
340    }
341    for block in exif_blocks(data) {
342        for (w, h) in [(Tag::PixelXDimension, Tag::PixelYDimension), (Tag::ImageWidth, Tag::ImageLength)] {
343            for ifd in [In::PRIMARY, In(1)] {
344                let get = |t: Tag| block.get_field(t, ifd).and_then(|f| f.value.get_uint(0));
345                if let (Some(w), Some(h)) = (get(w), get(h))
346                    && w > 0
347                    && h > 0
348                {
349                    out.push((w, h));
350                }
351            }
352        }
353    }
354    out
355}
356
357fn from_blocks(exif: &[::exif::Exif]) -> Option<CameraMeta> {
358    if exif.is_empty() {
359        return None;
360    }
361    let camera = match (text(exif, Tag::Make), text(exif, Tag::Model)) {
362        (None, None) => None,
363        (make, model) => Some(camera_name(make.as_deref().unwrap_or(""), model.as_deref().unwrap_or(""))),
364    };
365    let timestamp = text(exif, Tag::DateTimeOriginal)
366        .or_else(|| text(exif, Tag::DateTime))
367        .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S").ok())
368        .and_then(|d| Local.from_local_datetime(&d).earliest())
369        .map(|d| d.timestamp_millis());
370    let meta = CameraMeta {
371        timestamp,
372        camera,
373        iso: number(exif, Tag::PhotographicSensitivity).map(|v| v as u32).filter(|&v| v > 0),
374        focal_length: number(exif, Tag::FocalLength).filter(|&v| v > 0.0),
375        shutter: number(exif, Tag::ExposureTime).filter(|&v| v > 0.0),
376        aperture: number(exif, Tag::FNumber).filter(|&v| v > 0.0),
377    };
378    (!meta.is_empty()).then_some(meta)
379}
380
381#[cfg(test)]
382pub(crate) mod tests {
383    use super::*;
384
385    #[test]
386    fn dcraw_formats() {
387        assert_eq!(shutter_text(1.0 / 320.0), "1/320.0 sec");
388        assert_eq!(shutter_text(2.0), "2.0 sec");
389        assert_eq!(camera_name("NIKON CORPORATION", "NIKON D850"), "Nikon D850");
390        assert_eq!(camera_name("Canon", "Canon EOS R5"), "Canon EOS R5");
391        assert_eq!(camera_name("SONY", "ILCE-7M3"), "Sony ILCE-7M3");
392        assert_eq!(camera_name("Apple", "iPhone 15 Pro"), "Apple iPhone 15 Pro");
393        let m = CameraMeta {
394            timestamp: Some(1),
395            camera: Some("Pentax K-1".into()),
396            iso: Some(200),
397            focal_length: Some(200.0),
398            shutter: Some(1.0 / 320.0),
399            aperture: Some(3.2),
400        };
401        let v = serde_json::to_string(&m.to_raw_metas()).unwrap();
402        assert_eq!(
403            v,
404            r#"{"timestamp":1,"camera":"Pentax K-1","isoSpeed":200,"focalLength":"200.0 mm","shutter":"1/320.0 sec","aperture":"f/3.2"}"#
405        );
406    }
407
408    /// A little-endian TIFF with Make, Model and an EXIF IFD holding
409    /// ExposureTime, FNumber, ISO, DateTimeOriginal and FocalLength.
410    pub(crate) fn tiff_with_exif() -> Vec<u8> {
411        let mut t = Vec::new();
412        let push16 = |t: &mut Vec<u8>, v: u16| t.extend_from_slice(&v.to_le_bytes());
413        let push32 = |t: &mut Vec<u8>, v: u32| t.extend_from_slice(&v.to_le_bytes());
414        t.extend_from_slice(b"II*\0");
415        push32(&mut t, 8);
416        // IFD0 at 8: 3 entries -> 2 + 36 + 4 = 42 bytes, data from 50.
417        let make = b"NIKON CORPORATION\0";
418        let model = b"NIKON D850\0";
419        let (make_at, model_at) = (50u32, 50 + make.len() as u32);
420        let exif_at = model_at + model.len() as u32;
421        push16(&mut t, 3);
422        for (tag, typ, count, val) in [(0x010F, 2, make.len() as u32, make_at), (0x0110, 2, model.len() as u32, model_at), (0x8769, 4, 1, exif_at)] {
423            push16(&mut t, tag);
424            push16(&mut t, typ);
425            push32(&mut t, count);
426            push32(&mut t, val);
427        }
428        push32(&mut t, 0);
429        t.resize(50, 0);
430        t.extend_from_slice(make);
431        t.extend_from_slice(model);
432        // EXIF IFD: 5 entries -> 2 + 60 + 4 = 66 bytes, data after.
433        let date = b"2024:03:15 12:00:00\0";
434        let data_at = exif_at + 66;
435        let (exp_at, fnum_at, date_at, focal_at) = (data_at, data_at + 8, data_at + 16, data_at + 16 + date.len() as u32);
436        push16(&mut t, 5);
437        for (tag, typ, count, val) in
438            [(0x829A, 5, 1, exp_at), (0x829D, 5, 1, fnum_at), (0x8827, 3, 1, 400), (0x9003, 2, date.len() as u32, date_at), (0x920A, 5, 1, focal_at)]
439        {
440            push16(&mut t, tag);
441            push16(&mut t, typ);
442            push32(&mut t, count);
443            push32(&mut t, val);
444        }
445        push32(&mut t, 0);
446        for (n, d) in [(1, 250), (28, 10)] {
447            push32(&mut t, n);
448            push32(&mut t, d);
449        }
450        t.extend_from_slice(date);
451        push32(&mut t, 50);
452        push32(&mut t, 1);
453        t
454    }
455
456    #[test]
457    fn reads_tiff_exif() {
458        let dir = tempfile::tempdir().unwrap();
459        let p = dir.path().join("x.nef");
460        std::fs::write(&p, tiff_with_exif()).unwrap();
461        let m = read(&p).unwrap();
462        assert_eq!(m.camera.as_deref(), Some("Nikon D850"));
463        assert_eq!(m.iso, Some(400));
464        assert_eq!(m.aperture, Some(2.8));
465        assert_eq!(m.focal_length, Some(50.0));
466        assert_eq!(shutter_text(m.shutter.unwrap()), "1/250.0 sec");
467        let expected = Local.with_ymd_and_hms(2024, 3, 15, 12, 0, 0).unwrap().timestamp_millis();
468        assert_eq!(m.timestamp, Some(expected));
469    }
470
471    /// A little-endian TIFF with one IFD of `(tag, type, count, bytes)`
472    /// entries; values over four bytes go after the IFD.
473    fn tiff(entries: &[(u16, u16, u32, Vec<u8>)]) -> Vec<u8> {
474        let mut t = b"II*\0\x08\0\0\0".to_vec();
475        let mut data_at = 8 + 2 + entries.len() * 12 + 4;
476        let mut tail = Vec::new();
477        t.extend_from_slice(&(entries.len() as u16).to_le_bytes());
478        for (tag, typ, count, bytes) in entries {
479            t.extend_from_slice(&tag.to_le_bytes());
480            t.extend_from_slice(&typ.to_le_bytes());
481            t.extend_from_slice(&count.to_le_bytes());
482            if bytes.len() <= 4 {
483                let mut v = bytes.clone();
484                v.resize(4, 0);
485                t.extend_from_slice(&v);
486            } else {
487                t.extend_from_slice(&(data_at as u32).to_le_bytes());
488                tail.extend_from_slice(bytes);
489                data_at += bytes.len();
490            }
491        }
492        t.extend_from_slice(&[0; 4]);
493        t.extend(tail);
494        t
495    }
496
497    fn ascii(tag: u16, s: &str) -> (u16, u16, u32, Vec<u8>) {
498        let mut b = s.as_bytes().to_vec();
499        b.push(0);
500        (tag, 2, b.len() as u32, b)
501    }
502
503    fn rational(tag: u16, n: u32, d: u32) -> (u16, u16, u32, Vec<u8>) {
504        (tag, 5, 1, [n.to_le_bytes(), d.to_le_bytes()].concat())
505    }
506
507    fn mp4_box(kind: &[u8], payload: &[u8]) -> Vec<u8> {
508        [&((payload.len() + 8) as u32).to_be_bytes()[..], kind, payload].concat()
509    }
510
511    fn read_bytes(name: &str, bytes: &[u8]) -> Option<CameraMeta> {
512        let dir = tempfile::tempdir().unwrap();
513        let p = dir.path().join(name);
514        std::fs::write(&p, bytes).unwrap();
515        read(&p)
516    }
517
518    /// The facts [`tiff_with_exif`] holds, as `rawMetas`.
519    fn nikon_metas() -> String {
520        let t = Local.with_ymd_and_hms(2024, 3, 15, 12, 0, 0).unwrap().timestamp_millis();
521        format!(r#"{{"timestamp":{t},"camera":"Nikon D850","isoSpeed":400,"focalLength":"50.0 mm","shutter":"1/250.0 sec","aperture":"f/2.8"}}"#)
522    }
523
524    #[test]
525    fn reads_orf_and_rw2_with_their_own_magic() {
526        for magic in [*b"IIRO", *b"IIRS", *b"IIU\0"] {
527            let mut raw = tiff_with_exif();
528            raw[..4].copy_from_slice(&magic);
529            let m = read_bytes("x.orf", &raw).unwrap_or_else(|| panic!("{magic:?}"));
530            assert_eq!(serde_json::to_string(&m.to_raw_metas()).unwrap(), nikon_metas());
531        }
532    }
533
534    #[test]
535    fn reads_raf_from_its_jpeg_preview() {
536        let tiff = tiff_with_exif();
537        let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xE1];
538        jpeg.extend_from_slice(&((tiff.len() + 8) as u16).to_be_bytes());
539        jpeg.extend_from_slice(b"Exif\0\0");
540        jpeg.extend_from_slice(&tiff);
541        jpeg.extend_from_slice(&[0xFF, 0xD9]);
542        let mut raf = b"FUJIFILMCCD-RAW 0201FF383501".to_vec();
543        raf.resize(100, 0);
544        raf[84..88].copy_from_slice(&100u32.to_be_bytes());
545        raf[88..92].copy_from_slice(&(jpeg.len() as u32).to_be_bytes());
546        raf.extend_from_slice(&jpeg);
547        raf.extend_from_slice(&[0u8; 64]); // the CFA data would follow
548        let m = read_bytes("x.raf", &raf).unwrap();
549        assert_eq!(serde_json::to_string(&m.to_raw_metas()).unwrap(), nikon_metas());
550        // A preview pointer past the end gives nothing rather than garbage.
551        raf[84..88].copy_from_slice(&9999u32.to_be_bytes());
552        assert_eq!(read_bytes("x.raf", &raf), None);
553    }
554
555    #[test]
556    fn reads_cr3_canon_boxes() {
557        let cmt1 = tiff(&[ascii(0x010F, "Canon"), ascii(0x0110, "Canon EOS R5"), ascii(0x0132, "2024:01:02 03:04:05")]);
558        let cmt2 = tiff(&[
559            rational(0x829A, 1, 1000),
560            rational(0x829D, 40, 10),
561            (0x8827, 3, 1, 800u16.to_le_bytes().to_vec()),
562            ascii(0x9003, "2024:01:02 03:04:06"),
563            rational(0x920A, 85, 1),
564        ]);
565        let canon = [&CANON_UUID[..], &mp4_box(b"CNCV", b"CanonCR3_001/00.09.00/00.00.00"), &mp4_box(b"CMT1", &cmt1), &mp4_box(b"CMT2", &cmt2), &mp4_box(b"CMT3", b"II*\0")].concat();
566        let moov = mp4_box(b"moov", &[mp4_box(b"uuid", &canon), mp4_box(b"trak", &[0; 16])].concat());
567        let cr3 = [mp4_box(b"ftyp", b"crx \0\0\0\x01crx isom"), moov, mp4_box(b"mdat", &[0; 32])].concat();
568        let m = read_bytes("x.cr3", &cr3).unwrap();
569        let t = Local.with_ymd_and_hms(2024, 1, 2, 3, 4, 6).unwrap().timestamp_millis();
570        assert_eq!(
571            serde_json::to_string(&m.to_raw_metas()).unwrap(),
572            format!(r#"{{"timestamp":{t},"camera":"Canon EOS R5","isoSpeed":800,"focalLength":"85.0 mm","shutter":"1/1000.0 sec","aperture":"f/4.0"}}"#)
573        );
574        assert_eq!(orientation(&cr3), None);
575        // Without the Canon box there is nothing to read.
576        let bare = [mp4_box(b"ftyp", b"crx \0\0\0\x01crx isom"), mp4_box(b"moov", &mp4_box(b"trak", &[0; 16]))].concat();
577        assert_eq!(read_bytes("y.cr3", &bare), None);
578    }
579
580    /// Orientation comes out of RAF's JPEG preview and CR3's `CMT1`, as well
581    /// as plain TIFF-based files.
582    #[test]
583    fn orientation_of_raf_cr3_and_tiff() {
584        let turned = tiff(&[ascii(0x010F, "X"), (0x0112, 3, 1, 6u16.to_le_bytes().to_vec())]);
585        assert_eq!(orientation(&turned), Some(6));
586        let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xE1];
587        jpeg.extend_from_slice(&((turned.len() + 8) as u16).to_be_bytes());
588        jpeg.extend_from_slice(b"Exif\0\0");
589        jpeg.extend_from_slice(&turned);
590        jpeg.extend_from_slice(&[0xFF, 0xD9]);
591        let mut raf = b"FUJIFILMCCD-RAW 0201FF383501".to_vec();
592        raf.resize(100, 0);
593        raf[84..88].copy_from_slice(&100u32.to_be_bytes());
594        raf[88..92].copy_from_slice(&(jpeg.len() as u32).to_be_bytes());
595        raf.extend_from_slice(&jpeg);
596        assert_eq!(orientation(&raf), Some(6));
597        let canon = [&CANON_UUID[..], &mp4_box(b"CMT1", &tiff(&[(0x0112, 3, 1, 8u16.to_le_bytes().to_vec())]))].concat();
598        let cr3 = [mp4_box(b"ftyp", b"crx \0\0\0\x01crx isom"), mp4_box(b"moov", &mp4_box(b"uuid", &canon))].concat();
599        assert_eq!(orientation(&cr3), Some(8));
600        // Out-of-range values are ignored.
601        assert_eq!(orientation(&tiff(&[(0x0112, 3, 1, 9u16.to_le_bytes().to_vec())])), None);
602    }
603
604    /// A minimal MRW: `\0MRM`, data offset, then PRD, TTW and WBG blocks.
605    fn mrw(ttw: &[u8]) -> Vec<u8> {
606        let blk = |tag: &[u8; 4], body: &[u8]| [&tag[..], &(body.len() as u32).to_be_bytes(), body].concat();
607        let blocks = [blk(b"\0PRD", &[0; 24]), blk(b"\0TTW", ttw), blk(b"\0WBG", &[0; 12])].concat();
608        [&b"\0MRM"[..], &(blocks.len() as u32).to_be_bytes(), &blocks, &[0; 16]].concat()
609    }
610
611    fn jpeg_with_exif(tiff: &[u8]) -> Vec<u8> {
612        let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xE1];
613        jpeg.extend_from_slice(&((tiff.len() + 8) as u16).to_be_bytes());
614        jpeg.extend_from_slice(b"Exif\0\0");
615        jpeg.extend_from_slice(tiff);
616        jpeg.extend_from_slice(&[0xFF, 0xD9]);
617        jpeg
618    }
619
620    #[test]
621    fn orientation_of_mrw_ttw_block() {
622        for o in 1..=8u16 {
623            let raw = mrw(&tiff(&[(0x0112, 3, 1, o.to_le_bytes().to_vec())]));
624            assert_eq!(orientation(&raw), Some(u32::from(o)));
625        }
626        let ttw = tiff(&[ascii(0x010F, "Minolta")]);
627        assert_eq!(orientation(&mrw(&ttw)), None);
628        let raw = mrw(&tiff(&[(0x0112, 3, 1, 6u16.to_le_bytes().to_vec())]));
629        assert_eq!(orientation(&raw[..20]), None);
630        assert_eq!(orientation(&raw[..raw.len() / 2]), None);
631        // A block length past the data area is refused.
632        let mut bad = raw.clone();
633        bad[12..16].copy_from_slice(&u32::MAX.to_be_bytes());
634        assert_eq!(orientation(&bad), None);
635        assert_eq!(orientation(b"\0MRM"), None);
636    }
637
638    /// An X3F with a raw `IMAG` section and a preview section.
639    fn x3f(preview: &[u8]) -> Vec<u8> {
640        let mut f = b"FOVb".to_vec();
641        f.resize(104, 0);
642        let sec = |body: &[u8]| [&b"SECi"[..], &[0; 24], body].concat();
643        let raw = sec(&[0u8; 20]);
644        let prev = sec(preview);
645        let raw_at = f.len();
646        f.extend_from_slice(&raw);
647        let prev_at = f.len();
648        f.extend_from_slice(&prev);
649        let dir_at = f.len() as u32;
650        f.extend_from_slice(b"SECd");
651        f.extend_from_slice(&0x0002_0000u32.to_le_bytes());
652        f.extend_from_slice(&2u32.to_le_bytes());
653        for (at, len, kind) in [(raw_at, raw.len(), b"IMAG"), (prev_at, prev.len(), b"IMA2")] {
654            f.extend_from_slice(&(at as u32).to_le_bytes());
655            f.extend_from_slice(&(len as u32).to_le_bytes());
656            f.extend_from_slice(kind);
657        }
658        f.extend_from_slice(&dir_at.to_le_bytes());
659        f
660    }
661
662    #[test]
663    fn orientation_of_x3f_preview() {
664        for o in [1u16, 3, 6, 8] {
665            let raw = x3f(&jpeg_with_exif(&tiff(&[(0x0112, 3, 1, o.to_le_bytes().to_vec())])));
666            assert_eq!(orientation(&raw), Some(u32::from(o)));
667        }
668        let raw = x3f(&jpeg_with_exif(&tiff(&[(0x0112, 3, 1, 6u16.to_le_bytes().to_vec())])));
669        assert_eq!(orientation(&raw[..raw.len() - 8]), None);
670        assert_eq!(orientation(&raw[..60]), None);
671        assert_eq!(orientation(&x3f(&[1, 2, 3, 4])), None);
672        assert_eq!(orientation(b"FOVb"), None);
673        // A directory pointer past the end gives nothing.
674        let mut bad = raw.clone();
675        let n = bad.len();
676        bad[n - 4..].copy_from_slice(&u32::MAX.to_le_bytes());
677        assert_eq!(orientation(&bad), None);
678    }
679
680    /// A CRW: header, then a heap whose root table holds a sub-heap
681    /// (0x300a, image properties) with a 0x1810 record of `angle` degrees.
682    fn crw(angle: i32) -> Vec<u8> {
683        let entry = |tag: u16, size: usize, off: usize| [&tag.to_le_bytes()[..], &(size as u32).to_le_bytes(), &(off as u32).to_le_bytes()].concat();
684        // Inner heap: 26 bytes of ImageInfo, table, table offset.
685        let mut info = vec![0u8; 12];
686        info.extend_from_slice(&angle.to_le_bytes());
687        info.extend_from_slice(&[0; 10]);
688        let mut inner = info.clone();
689        let table_at = inner.len() as u32;
690        inner.extend_from_slice(&1u16.to_le_bytes());
691        inner.extend_from_slice(&entry(0x1810, info.len(), 0));
692        inner.extend_from_slice(&table_at.to_le_bytes());
693        // Outer heap: a decoy record, the inner heap, table, table offset.
694        let mut heap = vec![0xAA; 8];
695        let inner_at = heap.len();
696        heap.extend_from_slice(&inner);
697        let table_at = heap.len() as u32;
698        heap.extend_from_slice(&2u16.to_le_bytes());
699        heap.extend_from_slice(&entry(0x2005, 8, 0));
700        heap.extend_from_slice(&entry(0x300A, inner.len(), inner_at));
701        heap.extend_from_slice(&table_at.to_le_bytes());
702        [&b"II"[..], &26u32.to_le_bytes(), b"HEAPCCDR", &[0; 12], &heap].concat()
703    }
704
705    #[test]
706    fn size_of_crw_image_info() {
707        let mut raw = crw(0);
708        assert!(stated_sizes(&raw).is_empty(), "no size stated");
709        raw[34..38].copy_from_slice(&2048u32.to_le_bytes());
710        raw[38..42].copy_from_slice(&1536u32.to_le_bytes());
711        assert_eq!(stated_sizes(&raw), vec![(2048, 1536)]);
712        assert!(stated_sizes(&raw[..30]).is_empty());
713    }
714
715    #[test]
716    fn orientation_of_crw_image_info() {
717        for (angle, want) in [(0, Some(1)), (90, Some(6)), (180, Some(3)), (270, Some(8)), (-90, Some(8)), (45, None)] {
718            assert_eq!(orientation(&crw(angle)), want, "{angle}");
719        }
720        let raw = crw(90);
721        assert_eq!(orientation(&raw[..raw.len() - 1]), None);
722        assert_eq!(orientation(&raw[..20]), None);
723        assert_eq!(orientation(&raw[..14]), None);
724        // A header length or table offset past the end gives nothing.
725        let mut bad = raw.clone();
726        bad[2..6].copy_from_slice(&u32::MAX.to_le_bytes());
727        assert_eq!(orientation(&bad), None);
728        let mut bad = raw.clone();
729        let n = bad.len();
730        bad[n - 4..].copy_from_slice(&u32::MAX.to_le_bytes());
731        assert_eq!(orientation(&bad), None);
732        // A heap that nests itself ends at the depth cap.
733        let mut looped = raw.clone();
734        let n = looped.len();
735        looped[26..30].copy_from_slice(&[0; 4]);
736        looped[n - 4..].copy_from_slice(&(n as u32 - 26 - 4).to_le_bytes());
737        assert_eq!(orientation(&looped), None);
738        assert_eq!(orientation(b"II\x1a\0\0\0HEAPCCDR"), None);
739    }
740}