use std::fmt::Debug;
use nom::{
branch::alt, bytes::streaming::tag, combinator, number::Endianness, IResult, Needed, Parser,
};
use crate::{EntryValue, ExifEntry, ExifIter, ExifTag, GPSInfo, IfdIndex, TagOrCode};
use super::ifd::ParsedImageFileDirectory;
#[derive(Clone, Debug, PartialEq)]
pub struct Exif {
ifds: Vec<ParsedImageFileDirectory>,
gps_info: Option<GPSInfo>,
errors: Vec<(IfdIndex, TagOrCode, crate::EntryError)>,
has_embedded_track: bool,
}
impl Exif {
fn new(gps_info: Option<GPSInfo>, has_embedded_track: bool) -> Exif {
Exif {
ifds: Vec::new(),
gps_info,
errors: Vec::new(),
has_embedded_track,
}
}
pub fn get(&self, tag: ExifTag) -> Option<&EntryValue> {
self.get_in(IfdIndex::MAIN, tag)
}
pub fn get_in(&self, ifd: IfdIndex, tag: ExifTag) -> Option<&EntryValue> {
self.get_by_code(ifd, tag.code())
}
pub fn get_by_code(&self, ifd: IfdIndex, code: u16) -> Option<&EntryValue> {
self.ifds.get(ifd.as_usize()).and_then(|d| d.get(code))
}
pub fn iter(&self) -> impl Iterator<Item = ExifEntry<'_>> {
self.ifds.iter().enumerate().flat_map(|(idx, dir)| {
let ifd = IfdIndex::new(idx);
dir.iter().map(move |(code, value)| ExifEntry {
ifd,
tag: TagOrCode::from(code),
value,
})
})
}
pub fn gps_info(&self) -> Option<&GPSInfo> {
self.gps_info.as_ref()
}
pub fn errors(&self) -> &[(IfdIndex, TagOrCode, crate::EntryError)] {
&self.errors
}
pub fn has_embedded_track(&self) -> bool {
self.has_embedded_track
}
#[deprecated(
since = "3.1.0",
note = "renamed to `has_embedded_track` to reflect the actual semantics (paired track hint, not arbitrary embedded media)"
)]
pub fn has_embedded_media(&self) -> bool {
self.has_embedded_track()
}
fn put_value(&mut self, ifd: usize, code: u16, v: EntryValue) {
while self.ifds.len() < ifd + 1 {
self.ifds.push(ParsedImageFileDirectory::new());
}
self.ifds[ifd].put(code, v);
}
}
impl From<ExifIter> for Exif {
fn from(iter: ExifIter) -> Self {
let gps_info = iter.parse_gps().ok().flatten();
let has_embedded_track = iter.has_embedded_track();
let mut exif = Exif::new(gps_info, has_embedded_track);
for entry in iter {
let ifd = entry.ifd();
let tag = entry.tag();
let code = tag.code();
match entry.into_result() {
Ok(v) => exif.put_value(ifd.as_usize(), code, v),
Err(e) => exif.errors.push((ifd, tag, e)),
}
}
exif
}
}
pub(crate) const TIFF_HEADER_LEN: usize = 8;
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct TiffHeader {
pub endian: Endianness,
pub ifd0_offset: u32,
}
impl Default for TiffHeader {
fn default() -> Self {
Self {
endian: Endianness::Big,
ifd0_offset: 0,
}
}
}
impl Debug for TiffHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let endian_str = match self.endian {
Endianness::Big => "Big",
Endianness::Little => "Little",
Endianness::Native => "Native",
};
f.debug_struct("TiffHeader")
.field("endian", &endian_str)
.field("ifd0_offset", &format!("{:#x}", self.ifd0_offset))
.finish()
}
}
pub(crate) const IFD_ENTRY_SIZE: usize = 12;
impl TiffHeader {
pub fn parse(input: &[u8]) -> IResult<&[u8], TiffHeader> {
use nom::number::streaming::{u16, u32};
let (remain, endian) = TiffHeader::parse_endian(input)?;
let (_, (_, offset)) = (
combinator::verify(u16(endian), |magic| *magic == 0x2a),
u32(endian),
)
.parse(remain)?;
let header = Self {
endian,
ifd0_offset: offset,
};
Ok((remain, header))
}
pub fn parse_ifd_entry_num(input: &[u8], endian: Endianness) -> IResult<&[u8], u16> {
let (remain, num) = nom::number::streaming::u16(endian)(input)?; if num == 0 {
return Ok((remain, 0));
}
let size = (num as usize)
.checked_mul(IFD_ENTRY_SIZE)
.expect("should fit");
if size > remain.len() {
return Err(nom::Err::Incomplete(Needed::new(size - remain.len())));
}
Ok((remain, num))
}
fn parse_endian(input: &[u8]) -> IResult<&[u8], Endianness> {
combinator::map(alt((tag("MM"), tag("II"))), |endian_marker| {
if endian_marker == b"MM" {
Endianness::Big
} else {
Endianness::Little
}
})
.parse(input)
}
}
pub(crate) fn check_exif_header(data: &[u8]) -> Result<bool, nom::Err<nom::error::Error<&[u8]>>> {
tag::<_, _, nom::error::Error<_>>(EXIF_IDENT)(data).map(|_| true)
}
pub(crate) fn check_exif_header2(i: &[u8]) -> IResult<&[u8], ()> {
let (remain, _) = (
nom::number::complete::be_u32,
nom::bytes::complete::tag(EXIF_IDENT),
)
.parse(i)?;
Ok((remain, ()))
}
pub(crate) const EXIF_IDENT: &str = "Exif\0\0";
#[cfg(test)]
mod tests {
use std::io::Read;
use std::thread;
use test_case::test_case;
use crate::exif::input_into_iter;
use crate::jpeg::extract_exif_data;
use crate::slice::SubsliceRange;
use crate::testkit::{open_sample, read_sample};
use crate::ExifIterEntry;
use super::*;
#[test]
fn header() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let buf = [0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00];
let (_, header) = TiffHeader::parse(&buf).unwrap();
assert_eq!(
header,
TiffHeader {
endian: Endianness::Big,
ifd0_offset: 8,
}
);
}
#[test_case("exif.jpg")]
fn exif_iter_gps(path: &str) {
let buf = read_sample(path).unwrap();
let (_, data) = extract_exif_data(&buf).unwrap();
let range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
let data = bytes::Bytes::from(buf).slice(range);
let iter = input_into_iter(data, None).unwrap();
let gps = iter.parse_gps().unwrap().unwrap();
assert_eq!(gps.to_iso6709(), "+22.53113+114.02148/");
}
#[test_case("exif.jpg")]
fn clone_exif_iter_to_thread(path: &str) {
let buf = read_sample(path).unwrap();
let (_, data) = extract_exif_data(&buf).unwrap();
let range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
let data = bytes::Bytes::from(buf).slice(range);
let iter = input_into_iter(data, None).unwrap();
let iter2 = iter.clone();
let mut expect = String::new();
open_sample(&format!("{path}.txt"))
.unwrap()
.read_to_string(&mut expect)
.unwrap();
let jh = thread::spawn(move || iter_to_str(iter2));
let result = iter_to_str(iter);
assert_eq!(result.trim(), expect.trim());
assert_eq!(jh.join().unwrap().trim(), expect.trim());
}
fn iter_to_str(it: impl Iterator<Item = ExifIterEntry>) -> String {
let ss = it
.map(|x| {
format!(
"{}.{:<32} » {}",
x.ifd(),
match x.tag() {
crate::TagOrCode::Tag(t) => t.to_string(),
crate::TagOrCode::Unknown(c) => format!("Unknown(0x{c:04x})"),
},
x.result()
.map(|v| v.to_string())
.map_err(|e| e.to_string())
.unwrap_or_else(|s| s)
)
})
.collect::<Vec<String>>();
ss.join("\n")
}
#[test]
fn p5_baseline_exif_jpg_dump_snapshot() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let mut entries: Vec<String> = iter
.map(|e| {
let val = match e.result() {
Ok(v) => format!("{v}"),
Err(err) => format!("<err:{err}>"),
};
format!("{}.0x{:04x}={val}", e.ifd(), e.tag().code())
})
.collect();
entries.sort();
assert!(
entries.len() > 5,
"expected >5 entries, got {}",
entries.len()
);
assert!(
entries.iter().any(|s| s.contains("0x010f")),
"expected Make tag (0x010f) in snapshot, got {entries:?}"
);
}
#[test]
fn exif_get_in_main_routes_via_ifd_index() {
use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let v_via_get = exif.get(ExifTag::Model);
let v_via_get_in = exif.get_in(IfdIndex::MAIN, ExifTag::Model);
assert_eq!(v_via_get, v_via_get_in);
assert!(
v_via_get.is_some(),
"Model tag expected in testdata/exif.jpg"
);
}
#[test]
fn exif_get_by_code_finds_unrecognized_or_recognized_tag() {
use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let v = exif.get_by_code(IfdIndex::MAIN, ExifTag::Make.code());
assert!(v.is_some());
}
#[test]
fn exif_gps_info_returns_borrow_no_result_wrap() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let g: Option<&crate::GPSInfo> = exif.gps_info();
assert!(g.is_some(), "testdata/exif.jpg has GPS info");
assert_eq!(g.unwrap().to_iso6709(), "+22.53113+114.02148/");
}
#[test]
fn exif_iter_yields_main_ifd_entries() {
use crate::{ExifTag, IfdIndex, MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let main_count = exif.iter().filter(|e| e.ifd == IfdIndex::MAIN).count();
assert!(
main_count > 1,
"expected >1 entries in main IFD, got {main_count}"
);
for entry in exif.iter() {
let _: &crate::EntryValue = entry.value;
let code = entry.tag.code();
assert_eq!(
exif.get_by_code(entry.ifd, code).unwrap(),
entry.value,
"iter entry value should match get_by_code lookup"
);
}
let model_via_iter = exif
.iter()
.find(|e| e.tag.tag() == Some(ExifTag::Model))
.map(|e| e.value);
assert_eq!(model_via_iter, exif.get(ExifTag::Model));
}
#[test]
fn exif_errors_is_empty_for_clean_fixture() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/exif.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let errs: &[(crate::IfdIndex, crate::TagOrCode, crate::EntryError)] = exif.errors();
assert!(
errs.is_empty(),
"exif.jpg has no per-entry errors, got {errs:?}"
);
}
#[test]
fn exif_errors_captures_per_entry_errors_for_broken_fixture() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/broken.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
let exif: Exif = iter.into();
let _ = exif.errors();
}
#[test]
fn has_embedded_track_true_for_pixel_motion_photo() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/motion_photo_pixel_synth.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
assert!(
iter.has_embedded_track(),
"Pixel-style Motion Photo carries an embedded MP4 track"
);
let exif: Exif = iter.into();
assert!(exif.has_embedded_track(), "flag survives From<ExifIter>");
}
#[test]
fn has_embedded_track_false_for_plain_jpeg_and_heic() {
use crate::{MediaParser, MediaSource};
for path in ["testdata/exif.jpg", "testdata/exif.heic"] {
let mut parser = MediaParser::new();
let iter = parser.parse_exif(MediaSource::open(path).unwrap()).unwrap();
assert!(
!iter.has_embedded_track(),
"{path} has no Motion Photo / paired track signal"
);
let exif: Exif = iter.into();
assert!(!exif.has_embedded_track());
}
}
#[test]
#[allow(deprecated)]
fn deprecated_has_embedded_media_still_works() {
use crate::{MediaParser, MediaSource};
let mut parser = MediaParser::new();
let ms = MediaSource::open("testdata/motion_photo_pixel_synth.jpg").unwrap();
let iter = parser.parse_exif(ms).unwrap();
assert_eq!(iter.has_embedded_media(), iter.has_embedded_track());
let exif: Exif = iter.into();
assert_eq!(exif.has_embedded_media(), exif.has_embedded_track());
}
#[test]
fn parse_track_extracts_motion_photo_trailer() {
use crate::{MediaParser, MediaSource, TrackInfoTag};
let path = "testdata/motion_photo_pixel_synth.jpg";
let mut p1 = MediaParser::new();
let iter = p1.parse_exif(MediaSource::open(path).unwrap()).unwrap();
assert!(iter.has_embedded_track());
let mut p2 = MediaParser::new();
let track = p2
.parse_track(MediaSource::open(path).unwrap())
.expect("parse_track must extract the trailer MP4");
assert!(
track.get(TrackInfoTag::Width).is_some() || track.get(TrackInfoTag::Height).is_some(),
"trailer should yield at least one geometry tag"
);
}
#[test]
fn parse_track_on_plain_jpeg_returns_track_not_found() {
use crate::{Error, MediaParser, MediaSource};
let mut parser = MediaParser::new();
let err = parser
.parse_track(MediaSource::open("testdata/exif.jpg").unwrap())
.unwrap_err();
assert!(
matches!(err, Error::TrackNotFound),
"expected TrackNotFound, got {err:?}"
);
}
}