1use std::path::Path;
8
9use chrono::{Local, NaiveDateTime, TimeZone};
10use ::exif::{In, Tag, Value};
11use serde_json::{Map, Value as Json, json};
12
13pub 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
17pub 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#[derive(Debug, Clone, Default, PartialEq)]
26pub struct CameraMeta {
27 pub timestamp: Option<i64>,
29 pub camera: Option<String>,
31 pub iso: Option<u32>,
32 pub focal_length: Option<f64>,
34 pub shutter: Option<f64>,
36 pub aperture: Option<f64>,
38}
39
40impl CameraMeta {
41 pub fn is_empty(&self) -> bool {
42 *self == CameraMeta::default()
43 }
44
45 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
71pub 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
76const 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
82pub 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
101fn 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
126fn 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 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
156fn 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
186fn mrw_ttw(data: &[u8]) -> Option<&[u8]> {
191 let end = be32(data, 4)?.checked_add(8)?.min(data.len());
192 let mut at = 8usize;
193 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
210fn 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(§ion[28..]);
233 }
234 }
235 None
236}
237
238fn 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
264fn 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
274fn 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
307const CANON_UUID: [u8; 16] = [0x85, 0xC0, 0xB6, 0x87, 0x82, 0x0F, 0x11, 0xE0, 0x81, 0x11, 0xF4, 0xCE, 0x46, 0x2B, 0x6A, 0x48];
309
310fn 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
317pub fn read(path: &Path) -> Option<CameraMeta> {
319 let data = std::fs::read(path).ok()?;
320 from_blocks(&exif_blocks(&data))
321}
322
323pub 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
332pub 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 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 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 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 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 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]); let m = read_bytes("x.raf", &raf).unwrap();
549 assert_eq!(serde_json::to_string(&m.to_raw_metas()).unwrap(), nikon_metas());
550 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 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 #[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 assert_eq!(orientation(&tiff(&[(0x0112, 3, 1, 9u16.to_le_bytes().to_vec())])), None);
602 }
603
604 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 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 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 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 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 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 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 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 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}