use std::{fmt::Debug, sync::Arc};
use nom::{
number::{complete, Endianness},
sequence::tuple,
};
use thiserror::Error;
use crate::{
partial_vec::{AssociatedInput, PartialVec},
slice::SliceChecked,
values::{DataFormat, EntryData, IRational, ParseEntryError, URational},
EntryValue, ExifTag,
};
use super::{exif_exif::IFD_ENTRY_SIZE, tags::ExifTagCode, GPSInfo, TiffHeader};
#[tracing::instrument]
pub(crate) fn input_into_iter(
input: impl Into<PartialVec> + Debug,
state: Option<TiffHeader>,
) -> crate::Result<ExifIter> {
let input = input.into();
let (header, start) = match state {
Some(header) => (header, 0),
_ => {
let (_, header) = TiffHeader::parse(&input[..])?;
let start = header.ifd0_offset as usize;
if start > input.len() {
return Err(crate::Error::ParseFailed("no enough bytes".into()));
}
(header, start)
}
};
tracing::debug!(?header, offset = start);
let data = &input[..];
let mut ifd0 = IfdIter::try_new(
0,
input.partial(&data[start..]),
header.ifd0_offset,
header.endian,
None,
)?;
let tz = ifd0.find_tz_offset();
ifd0.tz = tz.clone();
let iter: ExifIter = ExifIter::new(input, header, tz, ifd0);
tracing::debug!(?iter, "got IFD0");
Ok(iter)
}
pub struct ExifIter {
input: Arc<PartialVec>,
tiff_header: TiffHeader,
tz: Option<String>,
ifd0: IfdIter,
ifds: Vec<IfdIter>,
}
impl Debug for ExifIter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExifIter")
.field("data len", &self.input.len())
.field("tiff_header", &self.tiff_header)
.field("ifd0", &self.ifd0)
.field("state", &self.ifds.first().map(|x| (x.index, x.pos)))
.field("ifds num", &self.ifds.len())
.finish_non_exhaustive()
}
}
impl Clone for ExifIter {
fn clone(&self) -> Self {
self.clone_and_rewind()
}
}
impl ExifIter {
pub(crate) fn new(
input: impl Into<PartialVec>,
tiff_header: TiffHeader,
tz: Option<String>,
ifd0: IfdIter,
) -> ExifIter {
let ifds = vec![ifd0.clone()];
ExifIter {
input: Arc::new(input.into()),
tiff_header,
tz,
ifd0,
ifds,
}
}
pub fn clone_and_rewind(&self) -> Self {
let ifd0 = self.ifd0.clone_and_rewind();
let ifds = vec![ifd0.clone()];
Self {
input: self.input.clone(),
tiff_header: self.tiff_header.clone(),
tz: self.tz.clone(),
ifd0,
ifds,
}
}
#[tracing::instrument(skip_all)]
pub fn parse_gps_info(&self) -> crate::Result<Option<GPSInfo>> {
let mut iter = self.clone_and_rewind();
let Some(gps) = iter.find(|x| {
tracing::info!(?x, "find");
x.tag.tag().is_some_and(|t| t == ExifTag::GPSInfo)
}) else {
tracing::warn!(ifd0 = ?iter.ifds.first(), "GPSInfo not found");
return Ok(None);
};
let offset = match gps.get_result() {
Ok(v) => {
if let Some(offset) = v.as_u32() {
offset
} else {
return Err(EntryError(ParseEntryError::InvalidData(
"invalid gps offset".into(),
))
.into());
}
}
Err(e) => return Err(e.clone().into()),
};
let mut gps_subifd = match IfdIter::try_new(
gps.ifd,
iter.input.partial(&iter.input[offset as usize..]),
offset,
iter.tiff_header.endian,
iter.tz.clone(),
) {
Ok(ifd0) => ifd0.tag_code(ExifTag::GPSInfo.code()),
Err(e) => return Err(e),
};
Ok(gps_subifd.parse_gps_info())
}
pub(crate) fn to_owned(&self) -> ExifIter {
ExifIter::new(
self.input.to_vec(),
self.tiff_header.clone(),
self.tz.clone(),
self.ifd0.clone_and_rewind(),
)
}
}
#[derive(Debug, Clone, Error)]
#[error("ifd entry error: {0}")]
pub struct EntryError(ParseEntryError);
impl From<EntryError> for crate::Error {
fn from(value: EntryError) -> Self {
Self::ParseFailed(value.into())
}
}
#[derive(Clone)]
pub struct ParsedExifEntry {
ifd: usize,
tag: ExifTagCode,
res: Option<Result<EntryValue, EntryError>>,
}
impl ParsedExifEntry {
pub fn ifd_index(&self) -> usize {
self.ifd
}
pub fn tag(&self) -> Option<ExifTag> {
match self.tag {
ExifTagCode::Tag(t) => Some(t),
ExifTagCode::Code(_) => None,
}
}
pub fn tag_code(&self) -> u16 {
self.tag.code()
}
pub fn has_value(&self) -> bool {
self.res.as_ref().map(|e| e.is_ok()).is_some_and(|b| b)
}
pub fn get_value(&self) -> Option<&EntryValue> {
match self.res.as_ref() {
Some(Ok(v)) => Some(v),
Some(Err(_)) | None => None,
}
}
pub fn take_value(&mut self) -> Option<EntryValue> {
match self.res.take() {
Some(v) => v.ok(),
None => None,
}
}
pub fn get_result(&self) -> Result<&EntryValue, &EntryError> {
match self.res {
Some(ref v) => v.as_ref(),
None => panic!("take result of entry twice"),
}
}
pub fn take_result(&mut self) -> Result<EntryValue, EntryError> {
match self.res.take() {
Some(v) => v,
None => panic!("take result of entry twice"),
}
}
fn make_ok(ifd: usize, tag: ExifTagCode, v: EntryValue) -> Self {
Self {
ifd,
tag,
res: Some(Ok(v)),
}
}
fn make_err(ifd: usize, tag: ExifTagCode, e: ParseEntryError) -> Self {
Self {
ifd,
tag,
res: Some(Err(EntryError(e))),
}
}
}
impl Debug for ParsedExifEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self.get_result() {
Ok(v) => format!("{v}"),
Err(e) => format!("{e:?}"),
};
f.debug_struct("IfdEntryResult")
.field("ifd", &format!("ifd{}", self.ifd))
.field("tag", &self.tag)
.field("value", &value)
.finish()
}
}
const MAX_IFD_DEPTH: usize = 8;
impl Iterator for ExifIter {
type Item = ParsedExifEntry;
#[tracing::instrument(skip_all)]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.ifds.is_empty() {
tracing::debug!(?self, "all IFDs has been parsed");
return None;
}
if self.ifds.len() > MAX_IFD_DEPTH {
self.ifds.clear();
tracing::error!(
ifds_depth = self.ifds.len(),
"ifd depth is too deep, just go back to ifd0"
);
self.ifds.push(self.ifd0.clone_with_state());
}
let mut ifd = self.ifds.pop()?;
let cur_ifd_idx = ifd.ifd_idx;
match ifd.next() {
Some((tag_code, entry)) => {
match entry {
IfdEntry::IfdNew(new_ifd) => {
let is_subifd = if new_ifd.ifd_idx == ifd.ifd_idx {
self.ifds.push(ifd);
tracing::debug!(?tag_code, ?new_ifd, "got new SUB-IFD");
true
} else {
tracing::debug!("IFD{} parsing completed", cur_ifd_idx);
tracing::debug!(?new_ifd, "got new IFD");
false
};
let (ifd_idx, offset) = (new_ifd.ifd_idx, new_ifd.offset);
self.ifds.push(new_ifd);
if is_subifd {
return Some(ParsedExifEntry::make_ok(
ifd_idx,
tag_code.unwrap(),
EntryValue::U32(offset),
));
}
}
IfdEntry::Entry(v) => {
let res =
Some(ParsedExifEntry::make_ok(ifd.ifd_idx, tag_code.unwrap(), v));
self.ifds.push(ifd);
return res;
}
IfdEntry::Err(e) => {
tracing::warn!(?tag_code, ?e, "parse ifd entry error");
let res =
Some(ParsedExifEntry::make_err(ifd.ifd_idx, tag_code.unwrap(), e));
self.ifds.push(ifd);
return res;
}
}
}
None => continue,
}
}
}
}
#[derive(Clone)]
pub(crate) struct IfdIter {
ifd_idx: usize,
tag_code: Option<ExifTagCode>,
input: AssociatedInput,
offset: u32,
pub tz: Option<String>,
endian: Endianness,
entry_num: u16,
index: u16,
pos: usize,
}
impl Debug for IfdIter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IfdIter")
.field("ifd_idx", &self.ifd_idx)
.field("tag", &self.tag_code)
.field("data len", &self.input.len())
.field("offset", &self.offset)
.field("tz", &self.tz)
.field("endian", &self.endian)
.field("entry_num", &self.entry_num)
.field("index", &self.index)
.field("pos", &self.pos)
.finish()
}
}
impl IfdIter {
pub fn rewind(&mut self) {
self.index = 0;
self.pos = 2;
}
pub fn clone_and_rewind(&self) -> Self {
let mut it = self.clone();
it.rewind();
it
}
pub fn tag_code_maybe(mut self, code: Option<u16>) -> Self {
self.tag_code = code.map(|x| x.into());
self
}
pub fn tag_code(mut self, code: u16) -> Self {
self.tag_code = Some(code.into());
self
}
#[allow(unused)]
pub fn tag(mut self, tag: ExifTagCode) -> Self {
self.tag_code = Some(tag);
self
}
#[tracing::instrument(skip(input))]
pub fn try_new(
ifd_idx: usize,
input: AssociatedInput,
offset: u32,
endian: Endianness,
tz: Option<String>,
) -> crate::Result<Self> {
if input.len() < 2 {
return Err(crate::Error::ParseFailed(
"ifd data is too small to decode entry num".into(),
));
}
let (_, entry_num) = TiffHeader::parse_ifd_entry_num(&input[..], endian)?;
Ok(Self {
ifd_idx,
tag_code: None,
input,
offset,
entry_num,
tz,
endian,
pos: 2,
index: 0,
})
}
fn parse_tag_entry(&self, entry_data: &[u8]) -> Option<(u16, IfdEntry)> {
let endian = self.endian;
let (_, (tag, data_format, components_num, value_or_offset)) = tuple((
complete::u16::<_, nom::error::Error<_>>(endian),
complete::u16(endian),
complete::u32(endian),
complete::u32(endian),
))(entry_data)
.ok()?;
if tag == 0 {
return None;
}
let df: DataFormat = match data_format.try_into() {
Ok(df) => df,
Err(e) => {
let t: ExifTagCode = tag.into();
tracing::warn!(tag = ?t, ?e, "invalid entry data format");
return Some((tag, IfdEntry::Err(e)));
}
};
let (tag, res) = self.parse_entry(tag, df, components_num, entry_data, value_or_offset);
Some((tag, res))
}
fn get_data_pos(&self, value_or_offset: u32) -> u32 {
value_or_offset.saturating_sub(self.offset)
}
fn parse_entry(
&self,
tag: u16,
data_format: DataFormat,
components_num: u32,
entry_data: &[u8],
value_or_offset: u32,
) -> (u16, IfdEntry) {
let component_size = data_format.component_size();
let size = components_num as usize * component_size;
let data = if size <= 4 {
&entry_data[8..8 + size] } else {
let start = self.get_data_pos(value_or_offset) as usize;
let end = start + size;
let Some(data) = self.input.slice_checked(start..end) else {
tracing::warn!(
"entry data overflow, self.offset: {:08x} tag: {:04x} start: {:08x} end: {:08x} ifd data len {:08x}",
self.offset,
tag,
start,
end,
self.input.len(),
);
return (tag, IfdEntry::Err(ParseEntryError::EntrySizeTooBig));
};
data
};
if SUBIFD_TAGS.contains(&tag) {
if let Some(value) = self.new_ifd_iter(self.ifd_idx, value_or_offset, Some(tag)) {
return (tag, value);
}
}
let entry = EntryData {
endian: self.endian,
tag,
data,
data_format,
components_num,
};
match EntryValue::parse(&entry, &self.tz) {
Ok(v) => (tag, IfdEntry::Entry(v)),
Err(e) => (tag, IfdEntry::Err(e)),
}
}
fn new_ifd_iter(
&self,
ifd_idx: usize,
value_or_offset: u32,
tag: Option<u16>,
) -> Option<IfdEntry> {
let pos = self.get_data_pos(value_or_offset) as usize;
if pos < self.input.len() {
match IfdIter::try_new(
ifd_idx,
self.input.partial(&self.input[pos..]),
value_or_offset,
self.endian,
self.tz.clone(),
) {
Ok(iter) => return Some(IfdEntry::IfdNew(iter.tag_code_maybe(tag))),
Err(e) => {
tracing::warn!(?tag, ?e, "Create next/sub IFD failed");
}
}
}
None
}
pub fn find_tz_offset(&self) -> Option<String> {
let endian = self.endian;
for i in 0..self.entry_num {
let pos = self.pos + i as usize * IFD_ENTRY_SIZE;
let (remain, tag) =
complete::u16::<_, nom::error::Error<_>>(endian)(&self.input[pos..]).ok()?;
if tag == ExifTag::ExifOffset.code() {
let (_, (_, _, offset)) = tuple((
complete::u16::<_, nom::error::Error<_>>(endian),
complete::u32(endian),
complete::u32(endian),
))(remain)
.ok()?;
return self.find_tz_offset_in_exif_subifd(offset);
}
}
None
}
fn find_tz_offset_in_exif_subifd(&self, offset: u32) -> Option<String> {
let num_entries = self.entry_num;
let pos = self.get_data_pos(offset + 2) as usize;
for i in 0..num_entries {
let pos = pos + i as usize * IFD_ENTRY_SIZE;
let entry_data = self.input.slice_checked(pos..pos + IFD_ENTRY_SIZE)?;
let (tag, res) = self.parse_tag_entry(entry_data)?;
if TZ_OFFSET_TAGS.contains(&tag) {
return match res {
IfdEntry::IfdNew(_) => {
tracing::warn!("got ifd when parsing tz offset");
continue;
}
IfdEntry::Entry(v) => match v {
EntryValue::Text(v) => return Some(v),
_ => {
tracing::warn!("tz offset is not a text");
continue;
}
},
IfdEntry::Err(_) => None,
};
}
}
None
}
pub fn parse_gps_info(&mut self) -> Option<GPSInfo> {
let mut gps = GPSInfo::default();
let mut has_data = false;
for (tag, entry) in self {
let Some(tag) = tag.and_then(|x| x.tag()) else {
continue;
};
has_data = true;
match tag {
ExifTag::GPSLatitudeRef => {
if let Some(c) = entry.as_char() {
gps.latitude_ref = c;
}
}
ExifTag::GPSLongitudeRef => {
if let Some(c) = entry.as_char() {
gps.longitude_ref = c;
}
}
ExifTag::GPSAltitudeRef => {
if let Some(c) = entry.as_u8() {
gps.altitude_ref = c;
}
}
ExifTag::GPSLatitude => {
if let Some(v) = entry.as_urational_array() {
gps.latitude = v.iter().collect();
} else if let Some(v) = entry.as_irational_array() {
gps.latitude = v.iter().collect();
}
}
ExifTag::GPSLongitude => {
if let Some(v) = entry.as_urational_array() {
gps.longitude = v.iter().collect();
} else if let Some(v) = entry.as_irational_array() {
gps.longitude = v.iter().collect();
}
}
ExifTag::GPSAltitude => {
if let Some(v) = entry.as_urational() {
gps.altitude = *v;
} else if let Some(v) = entry.as_irational() {
gps.altitude = (*v).into();
}
}
ExifTag::GPSSpeedRef => {
if let Some(c) = entry.as_char() {
gps.speed_ref = Some(c);
}
}
ExifTag::GPSSpeed => {
if let Some(v) = entry.as_urational() {
gps.speed = Some(*v);
} else if let Some(v) = entry.as_irational() {
gps.speed = Some((*v).into());
}
}
_ => (),
}
}
if has_data {
Some(gps)
} else {
tracing::warn!("GPSInfo data not found");
None
}
}
fn clone_with_state(&self) -> IfdIter {
let mut it = self.clone();
it.index = self.index;
it.pos = self.pos;
it
}
}
#[derive(Debug)]
pub(crate) enum IfdEntry {
IfdNew(IfdIter), Entry(EntryValue),
Err(ParseEntryError),
}
impl IfdEntry {
pub fn as_u8(&self) -> Option<u8> {
if let IfdEntry::Entry(EntryValue::U8(v)) = self {
Some(*v)
} else {
None
}
}
pub fn as_char(&self) -> Option<char> {
if let IfdEntry::Entry(EntryValue::Text(s)) = self {
s.chars().next()
} else {
None
}
}
fn as_irational(&self) -> Option<&IRational> {
if let IfdEntry::Entry(EntryValue::IRational(v)) = self {
Some(v)
} else {
None
}
}
fn as_irational_array(&self) -> Option<&Vec<IRational>> {
if let IfdEntry::Entry(EntryValue::IRationalArray(v)) = self {
Some(v)
} else {
None
}
}
fn as_urational(&self) -> Option<&URational> {
if let IfdEntry::Entry(EntryValue::URational(v)) = self {
Some(v)
} else {
None
}
}
fn as_urational_array(&self) -> Option<&Vec<URational>> {
if let IfdEntry::Entry(EntryValue::URationalArray(v)) = self {
Some(v)
} else {
None
}
}
}
pub(crate) const SUBIFD_TAGS: &[u16] = &[ExifTag::ExifOffset.code(), ExifTag::GPSInfo.code()];
const TZ_OFFSET_TAGS: &[u16] = &[
ExifTag::OffsetTimeOriginal.code(),
ExifTag::OffsetTimeDigitized.code(),
ExifTag::OffsetTime.code(),
];
impl Iterator for IfdIter {
type Item = (Option<ExifTagCode>, IfdEntry);
#[tracing::instrument(skip(self))]
fn next(&mut self) -> Option<Self::Item> {
if self.input.len() < self.pos + IFD_ENTRY_SIZE {
return None;
}
let endian = self.endian;
if self.index > self.entry_num {
return None;
}
if self.index == self.entry_num {
tracing::debug!(
self.ifd_idx,
self.index,
pos = self.pos,
"try to get next ifd"
);
self.index += 1;
let (_, offset) =
complete::u32::<_, nom::error::Error<_>>(endian)(&self.input[self.pos..]).ok()?;
if offset == 0 {
tracing::debug!(?self, "IFD parsing completed");
return None;
}
return self
.new_ifd_iter(self.ifd_idx + 1, offset, None)
.map(|x| (None, x));
}
let entry_data = self
.input
.slice_checked(self.pos..self.pos + IFD_ENTRY_SIZE)?;
self.index += 1;
self.pos += IFD_ENTRY_SIZE;
let (tag, res) = self.parse_tag_entry(entry_data)?;
Some((Some(tag.into()), res)) }
}
#[cfg(test)]
mod tests {
use crate::exif::extract_exif_with_mime;
use crate::exif::input_into_iter;
use crate::file::MimeImage;
use crate::slice::SubsliceRange;
use crate::testkit::read_sample;
use test_case::test_case;
#[test_case("exif.jpg", "+08:00", MimeImage::Jpeg)]
#[test_case("broken.jpg", "", MimeImage::Jpeg)]
#[test_case("exif.heic", "+08:00", MimeImage::Heic)]
#[test_case("tif.tif", "", MimeImage::Tiff)]
fn exif_iter_tz(path: &str, tz: &str, img_type: MimeImage) {
let buf = read_sample(path).unwrap();
let (data, _) = extract_exif_with_mime(img_type, &buf, None).unwrap();
let subslice_range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
let iter = input_into_iter((buf, subslice_range), None).unwrap();
let expect = if tz.is_empty() {
None
} else {
Some(tz.to_string())
};
assert_eq!(iter.tz, expect);
}
}