1use std::fmt::Debug;
2
3use nom::{
4 branch::alt, bytes::streaming::tag, combinator, number::Endianness, IResult, Needed, Parser,
5};
6
7use crate::{
8 EntryValue, ExifEntry, ExifEntryRef, ExifIter, ExifTag, GPSInfo, IfdIndex, IfdKind, TagOrCode,
9};
10
11use super::ifd::ParsedImageFileDirectory;
12
13#[derive(Clone, Debug, PartialEq)]
16pub struct Exif {
17 ifds: Vec<ParsedImageFileDirectory>,
18 gps_info: Option<GPSInfo>,
19 errors: Vec<(IfdIndex, TagOrCode, crate::EntryError)>,
20 has_embedded_track: bool,
21}
22
23impl Exif {
24 fn new(gps_info: Option<GPSInfo>, has_embedded_track: bool) -> Exif {
25 Exif {
26 ifds: Vec::new(),
27 gps_info,
28 errors: Vec::new(),
29 has_embedded_track,
30 }
31 }
32
33 pub fn get(&self, tag: ExifTag) -> Option<&EntryValue> {
65 self.get_in(IfdIndex::MAIN, tag)
66 }
67
68 pub fn get_in(&self, ifd: IfdIndex, tag: ExifTag) -> Option<&EntryValue> {
96 match tag.namespace() {
97 Some(kind) => self.get_by_code_in(ifd, kind, tag.code()),
100 None => self.get_by_code(ifd, tag.code()),
101 }
102 }
103
104 pub fn get_by_code(&self, ifd: IfdIndex, code: u16) -> Option<&EntryValue> {
111 self.ifds.get(ifd.as_usize()).and_then(|d| d.get_any(code))
112 }
113
114 pub fn get_by_code_in(&self, ifd: IfdIndex, kind: IfdKind, code: u16) -> Option<&EntryValue> {
119 self.ifds
120 .get(ifd.as_usize())
121 .and_then(|d| d.get(kind, code))
122 }
123
124 pub fn entries(&self) -> impl Iterator<Item = ExifEntryRef<'_>> {
134 self.ifds.iter().enumerate().flat_map(|(idx, dir)| {
135 let ifd = IfdIndex::new(idx);
136 dir.iter().map(move |(kind, code, value)| {
137 ExifEntryRef::new(ifd, kind, TagOrCode::from_code_in(kind, code), value)
138 })
139 })
140 }
141
142 #[deprecated(
147 since = "3.8.0",
148 note = "use `entries()`: `ExifEntry` has no room for the IFD namespace, so tags that share a code across IFDs (e.g. 0x000b = ProcessingSoftware in IFD0, GPSDOP in the GPS IFD) are indistinguishable here"
149 )]
150 pub fn iter(&self) -> impl Iterator<Item = ExifEntry<'_>> {
151 self.ifds.iter().enumerate().flat_map(|(idx, dir)| {
152 let ifd = IfdIndex::new(idx);
153 dir.iter().map(move |(kind, code, value)| ExifEntry {
154 ifd,
155 tag: TagOrCode::from_code_in(kind, code),
156 value,
157 })
158 })
159 }
160
161 pub fn gps_info(&self) -> Option<&GPSInfo> {
166 self.gps_info.as_ref()
167 }
168
169 pub fn errors(&self) -> &[(IfdIndex, TagOrCode, crate::EntryError)] {
172 &self.errors
173 }
174
175 pub fn has_embedded_track(&self) -> bool {
191 self.has_embedded_track
192 }
193
194 #[deprecated(
196 since = "3.1.0",
197 note = "renamed to `has_embedded_track` to reflect the actual semantics (paired track hint, not arbitrary embedded media)"
198 )]
199 pub fn has_embedded_media(&self) -> bool {
200 self.has_embedded_track()
201 }
202
203 fn put_value(&mut self, ifd: usize, kind: IfdKind, code: u16, v: EntryValue) {
204 while self.ifds.len() < ifd + 1 {
205 self.ifds.push(ParsedImageFileDirectory::new());
206 }
207 self.ifds[ifd].put(kind, code, v);
208 }
209}
210
211impl From<ExifIter> for Exif {
212 fn from(iter: ExifIter) -> Self {
213 let gps_info = iter.parse_gps().ok().flatten();
214 let has_embedded_track = iter.has_embedded_track();
215 let mut exif = Exif::new(gps_info, has_embedded_track);
216
217 for entry in iter {
218 let ifd = entry.ifd();
219 let kind = entry.ifd_kind();
220 let tag = entry.tag();
221 let code = tag.code();
222 match entry.into_result() {
223 Ok(v) => exif.put_value(ifd.as_usize(), kind, code, v),
224 Err(e) => exif.errors.push((ifd, tag, e)),
225 }
226 }
227
228 exif
229 }
230}
231
232pub(crate) const TIFF_HEADER_LEN: usize = 8;
233
234#[derive(Clone, PartialEq, Eq)]
236pub(crate) struct TiffHeader {
237 pub endian: Endianness,
238 pub ifd0_offset: u32,
239}
240
241impl Default for TiffHeader {
242 fn default() -> Self {
243 Self {
244 endian: Endianness::Big,
245 ifd0_offset: 0,
246 }
247 }
248}
249
250impl Debug for TiffHeader {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 let endian_str = match self.endian {
253 Endianness::Big => "Big",
254 Endianness::Little => "Little",
255 Endianness::Native => "Native",
256 };
257 f.debug_struct("TiffHeader")
258 .field("endian", &endian_str)
259 .field("ifd0_offset", &format!("{:#x}", self.ifd0_offset))
260 .finish()
261 }
262}
263
264pub(crate) const IFD_ENTRY_SIZE: usize = 12;
265
266impl TiffHeader {
267 pub fn parse(input: &[u8]) -> IResult<&[u8], TiffHeader> {
268 use nom::number::streaming::{u16, u32};
269 let (remain, endian) = TiffHeader::parse_endian(input)?;
270 let (_, (_, offset)) = (
271 combinator::verify(u16(endian), |magic| *magic == 0x2a),
272 u32(endian),
273 )
274 .parse(remain)?;
275
276 let header = Self {
277 endian,
278 ifd0_offset: offset,
279 };
280
281 Ok((remain, header))
282 }
283
284 pub fn parse_ifd_entry_num(input: &[u8], endian: Endianness) -> IResult<&[u8], u16> {
285 let (remain, num) = nom::number::streaming::u16(endian)(input)?; if num == 0 {
287 return Ok((remain, 0));
288 }
289
290 let size = (num as usize)
292 .checked_mul(IFD_ENTRY_SIZE)
293 .expect("should fit");
294
295 if size > remain.len() {
296 return Err(nom::Err::Incomplete(Needed::new(size - remain.len())));
297 }
298
299 Ok((remain, num))
300 }
301
302 fn parse_endian(input: &[u8]) -> IResult<&[u8], Endianness> {
314 combinator::map(alt((tag("MM"), tag("II"))), |endian_marker| {
315 if endian_marker == b"MM" {
316 Endianness::Big
317 } else {
318 Endianness::Little
319 }
320 })
321 .parse(input)
322 }
323}
324
325pub(crate) fn check_exif_header(data: &[u8]) -> Result<bool, nom::Err<nom::error::Error<&[u8]>>> {
326 tag::<_, _, nom::error::Error<_>>(EXIF_IDENT)(data).map(|_| true)
327}
328
329pub(crate) fn check_exif_header2(i: &[u8]) -> IResult<&[u8], ()> {
330 let (remain, _) = (
331 nom::number::complete::be_u32,
332 nom::bytes::complete::tag(EXIF_IDENT),
333 )
334 .parse(i)?;
335 Ok((remain, ()))
336}
337
338pub(crate) const EXIF_IDENT: &str = "Exif\0\0";
339
340#[cfg(test)]
341mod tests {
342 use std::io::Read;
343 use std::thread;
344
345 use test_case::test_case;
346
347 use crate::exif::input_into_iter;
348 use crate::jpeg::extract_exif_data;
349 use crate::slice::SubsliceRange;
350 use crate::testkit::{open_sample, read_sample};
351 use crate::ExifIterEntry;
352
353 use super::*;
354
355 #[test]
356 fn header() {
357 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
358
359 let buf = [0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00];
360
361 let (_, header) = TiffHeader::parse(&buf).unwrap();
362 assert_eq!(
363 header,
364 TiffHeader {
365 endian: Endianness::Big,
366 ifd0_offset: 8,
367 }
368 );
369 }
370
371 #[test_case("exif.jpg")]
372 fn exif_iter_gps(path: &str) {
373 let buf = read_sample(path).unwrap();
374 let (_, data) = extract_exif_data(&buf).unwrap();
375 let range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
376 let data = bytes::Bytes::from(buf).slice(range);
377 let iter = input_into_iter(data, None).unwrap();
378 let gps = iter.parse_gps().unwrap().unwrap();
379 assert_eq!(gps.to_iso6709(), "+22.53113+114.02148/");
380 }
381
382 #[test_case("exif.jpg")]
383 fn clone_exif_iter_to_thread(path: &str) {
384 let buf = read_sample(path).unwrap();
385 let (_, data) = extract_exif_data(&buf).unwrap();
386 let range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
387 let data = bytes::Bytes::from(buf).slice(range);
388 let iter = input_into_iter(data, None).unwrap();
389 let iter2 = iter.clone();
390
391 let mut expect = String::new();
392 open_sample(&format!("{path}.txt"))
393 .unwrap()
394 .read_to_string(&mut expect)
395 .unwrap();
396
397 let jh = thread::spawn(move || iter_to_str(iter2));
398
399 let result = iter_to_str(iter);
400
401 assert_eq!(result.trim(), expect.trim());
409 assert_eq!(jh.join().unwrap().trim(), expect.trim());
410 }
411
412 fn iter_to_str(it: impl Iterator<Item = ExifIterEntry>) -> String {
413 let ss = it
414 .map(|x| {
415 format!(
416 "{}/{:<7}.{:<32} » {}",
417 x.ifd(),
418 x.ifd_kind(),
419 match x.tag() {
420 crate::TagOrCode::Tag(t) => t.to_string(),
421 crate::TagOrCode::Unknown(c) => format!("Unknown(0x{c:04x})"),
422 },
423 x.result()
424 .map(|v| v.to_string())
425 .map_err(|e| e.to_string())
426 .unwrap_or_else(|s| s)
427 )
428 })
429 .collect::<Vec<String>>();
430 ss.join("\n")
431 }
432
433 #[test]
434 fn p5_baseline_exif_jpg_dump_snapshot() {
435 use crate::{MediaParser, MediaSource};
440 let mut parser = MediaParser::new();
441 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
442 let iter = parser.parse_exif(ms).unwrap();
443
444 let mut entries: Vec<String> = iter
445 .map(|e| {
446 let val = match e.result() {
447 Ok(v) => format!("{v}"),
448 Err(err) => format!("<err:{err}>"),
449 };
450 format!("{}.0x{:04x}={val}", e.ifd(), e.tag().code())
451 })
452 .collect();
453 entries.sort();
454 assert!(
455 entries.len() > 5,
456 "expected >5 entries, got {}",
457 entries.len()
458 );
459 assert!(
460 entries.iter().any(|s| s.contains("0x010f")),
461 "expected Make tag (0x010f) in snapshot, got {entries:?}"
462 );
463 }
464
465 #[test]
466 fn exif_get_in_main_routes_via_ifd_index() {
467 use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
468 let mut parser = MediaParser::new();
469 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
470 let iter = parser.parse_exif(ms).unwrap();
471 let exif: Exif = iter.into();
472
473 let v_via_get = exif.get(ExifTag::Model);
475 let v_via_get_in = exif.get_in(IfdIndex::MAIN, ExifTag::Model);
476 assert_eq!(v_via_get, v_via_get_in);
477 assert!(
478 v_via_get.is_some(),
479 "Model tag expected in testdata/exif.jpg"
480 );
481 }
482
483 #[test]
484 fn exif_get_by_code_finds_unrecognized_or_recognized_tag() {
485 use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
486 let mut parser = MediaParser::new();
487 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
488 let iter = parser.parse_exif(ms).unwrap();
489 let exif: Exif = iter.into();
490 let v = exif.get_by_code(IfdIndex::MAIN, ExifTag::Make.code());
492 assert!(v.is_some());
493 }
494
495 #[test]
496 fn exif_gps_info_returns_borrow_no_result_wrap() {
497 use crate::{MediaParser, MediaSource};
498 let mut parser = MediaParser::new();
499 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
500 let iter = parser.parse_exif(ms).unwrap();
501 let exif: Exif = iter.into();
502 let g: Option<&crate::GPSInfo> = exif.gps_info();
504 assert!(g.is_some(), "testdata/exif.jpg has GPS info");
505 assert_eq!(g.unwrap().to_iso6709(), "+22.53113+114.02148/");
506 }
507
508 #[test]
509 fn exif_iter_yields_main_ifd_entries() {
510 use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
511 let mut parser = MediaParser::new();
512 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
513 let iter = parser.parse_exif(ms).unwrap();
514 let exif: Exif = iter.into();
515
516 #[allow(deprecated)]
517 let main_count = exif.iter().filter(|e| e.ifd == IfdIndex::MAIN).count();
518 assert!(
519 main_count > 1,
520 "expected >1 entries in main IFD, got {main_count}"
521 );
522
523 #[allow(deprecated)]
528 for entry in exif.iter() {
529 let _: &crate::EntryValue = entry.value;
530 assert!(
531 exif.get_by_code(entry.ifd, entry.tag.code()).is_some(),
532 "every iterated code should resolve to some entry"
533 );
534 }
535
536 #[allow(deprecated)]
538 let model_via_iter = exif
539 .iter()
540 .find(|e| e.tag.tag() == Some(ExifTag::Model))
541 .map(|e| e.value);
542 assert_eq!(model_via_iter, exif.get(ExifTag::Model));
543 }
544
545 #[test]
546 fn entries_round_trip_through_get_by_code_in() {
547 use crate::{MediaParser, MediaSource};
548 let mut parser = MediaParser::new();
549 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
550 let iter = parser.parse_exif(ms).unwrap();
551 let exif: Exif = iter.into();
552
553 for e in exif.entries() {
556 assert_eq!(
557 exif.get_by_code_in(e.ifd(), e.ifd_kind(), e.tag().code()),
558 Some(e.value()),
559 "entry {:?} in {}/{} should round-trip",
560 e.tag(),
561 e.ifd(),
562 e.ifd_kind()
563 );
564 }
565 }
566
567 #[test]
568 fn exif_errors_is_empty_for_clean_fixture() {
569 use crate::{MediaParser, MediaSource};
570 let mut parser = MediaParser::new();
571 let ms = MediaSource::open("testdata/exif.jpg").unwrap();
572 let iter = parser.parse_exif(ms).unwrap();
573 let exif: Exif = iter.into();
574 let errs: &[(crate::IfdIndex, crate::TagOrCode, crate::EntryError)] = exif.errors();
577 assert!(
578 errs.is_empty(),
579 "exif.jpg has no per-entry errors, got {errs:?}"
580 );
581 }
582
583 #[test]
584 fn exif_errors_captures_per_entry_errors_for_broken_fixture() {
585 use crate::{MediaParser, MediaSource};
586 let mut parser = MediaParser::new();
587 let ms = MediaSource::open("testdata/broken.jpg").unwrap();
588 let iter = parser.parse_exif(ms).unwrap();
589 let exif: Exif = iter.into();
590 let _ = exif.errors();
594 }
595
596 #[test]
597 fn has_embedded_track_true_for_pixel_motion_photo() {
598 use crate::{MediaParser, MediaSource};
599 let mut parser = MediaParser::new();
600 let ms = MediaSource::open("testdata/motion_photo_pixel_synth.jpg").unwrap();
601 let iter = parser.parse_exif(ms).unwrap();
602 assert!(
603 iter.has_embedded_track(),
604 "Pixel-style Motion Photo carries an embedded MP4 track"
605 );
606 let exif: Exif = iter.into();
607 assert!(exif.has_embedded_track(), "flag survives From<ExifIter>");
608 }
609
610 #[test]
611 fn has_embedded_track_false_for_plain_jpeg_and_heic() {
612 use crate::{MediaParser, MediaSource};
613 for path in ["testdata/exif.jpg", "testdata/exif.heic"] {
614 let mut parser = MediaParser::new();
615 let iter = parser.parse_exif(MediaSource::open(path).unwrap()).unwrap();
616 assert!(
617 !iter.has_embedded_track(),
618 "{path} has no Motion Photo / paired track signal"
619 );
620 let exif: Exif = iter.into();
621 assert!(!exif.has_embedded_track());
622 }
623 }
624
625 #[test]
626 #[allow(deprecated)]
627 fn deprecated_has_embedded_media_still_works() {
628 use crate::{MediaParser, MediaSource};
629 let mut parser = MediaParser::new();
630 let ms = MediaSource::open("testdata/motion_photo_pixel_synth.jpg").unwrap();
631 let iter = parser.parse_exif(ms).unwrap();
632 assert_eq!(iter.has_embedded_media(), iter.has_embedded_track());
634 let exif: Exif = iter.into();
635 assert_eq!(exif.has_embedded_media(), exif.has_embedded_track());
636 }
637
638 #[test]
642 fn parse_track_extracts_motion_photo_trailer() {
643 use crate::{MediaParser, MediaSource, TrackInfoTag};
644 let path = "testdata/motion_photo_pixel_synth.jpg";
645
646 let mut p1 = MediaParser::new();
647 let iter = p1.parse_exif(MediaSource::open(path).unwrap()).unwrap();
648 assert!(iter.has_embedded_track());
649
650 let mut p2 = MediaParser::new();
651 let track = p2
652 .parse_track(MediaSource::open(path).unwrap())
653 .expect("parse_track must extract the trailer MP4");
654 assert!(
655 track.get(TrackInfoTag::Width).is_some() || track.get(TrackInfoTag::Height).is_some(),
656 "trailer should yield at least one geometry tag"
657 );
658 }
659
660 #[test]
662 fn parse_track_on_plain_jpeg_returns_track_not_found() {
663 use crate::{Error, MediaParser, MediaSource};
664 let mut parser = MediaParser::new();
665 let err = parser
666 .parse_track(MediaSource::open("testdata/exif.jpg").unwrap())
667 .unwrap_err();
668 assert!(
669 matches!(err, Error::TrackNotFound),
670 "expected TrackNotFound, got {err:?}"
671 );
672 }
673}