Skip to content

Commit 3cca4b2

Browse files
committed
feat(exif): add EntryValue::NaiveDateTime #38 #39
1 parent cfcfc51 commit 3cca4b2

File tree

5 files changed

+80
-22
lines changed

5 files changed

+80
-22
lines changed

src/exif/exif_exif.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::Debug;
2+
13
use nom::{
24
branch::alt, bytes::streaming::tag, combinator, number::Endianness, sequence, IResult, Needed,
35
};
@@ -155,7 +157,7 @@ impl From<ExifIter> for Exif {
155157
}
156158

157159
/// TIFF Header
158-
#[derive(Clone, Debug, PartialEq, Eq)]
160+
#[derive(Clone, PartialEq, Eq)]
159161
pub(crate) struct TiffHeader {
160162
pub endian: Endianness,
161163
pub ifd0_offset: u32,
@@ -170,6 +172,20 @@ impl Default for TiffHeader {
170172
}
171173
}
172174

175+
impl Debug for TiffHeader {
176+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177+
let endian_str = match self.endian {
178+
Endianness::Big => "Big",
179+
Endianness::Little => "Little",
180+
Endianness::Native => "Native",
181+
};
182+
f.debug_struct("TiffHeader")
183+
.field("endian", &endian_str)
184+
.field("ifd0_offset", &format!("{:#x}", self.ifd0_offset))
185+
.finish()
186+
}
187+
}
188+
173189
pub(crate) const IFD_ENTRY_SIZE: usize = 12;
174190

175191
impl TiffHeader {

src/exif/exif_iter.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ pub(crate) fn input_into_iter(
4141
return Err(crate::Error::ParseFailed("no enough bytes".into()));
4242
}
4343

44+
tracing::debug!(
45+
?header,
46+
data_len = format!("{:#x}", input.len()),
47+
"TIFF header parsed"
48+
);
4449
(header, start)
4550
}
4651
};
@@ -895,23 +900,38 @@ mod tests {
895900
use crate::file::MimeImage;
896901
use crate::slice::SubsliceRange;
897902
use crate::testkit::read_sample;
903+
use crate::Exif;
898904
use test_case::test_case;
899905

900-
#[test_case("exif.jpg", "+08:00", MimeImage::Jpeg)]
901-
#[test_case("broken.jpg", "", MimeImage::Jpeg)]
902-
#[test_case("exif.heic", "+08:00", MimeImage::Heic)]
903-
#[test_case("tif.tif", "", MimeImage::Tiff)]
904-
#[test_case("fujifilm_x_t1_01.raf.meta", "", MimeImage::Raf)]
905-
fn exif_iter_tz(path: &str, tz: &str, img_type: MimeImage) {
906+
#[test_case("exif.jpg", "+08:00", "2023-07-09T20:36:33+08:00", MimeImage::Jpeg)]
907+
#[test_case("exif-no-tz.jpg", "", "2023-07-09 20:36:33", MimeImage::Jpeg)]
908+
#[test_case("broken.jpg", "-", "2014-09-21 15:51:22", MimeImage::Jpeg)]
909+
#[test_case("exif.heic", "+08:00", "2022-07-22T21:26:32+08:00", MimeImage::Heic)]
910+
#[test_case("tif.tif", "-", "-", MimeImage::Tiff)]
911+
#[test_case(
912+
"fujifilm_x_t1_01.raf.meta",
913+
"-",
914+
"2014-01-30 12:49:13",
915+
MimeImage::Raf
916+
)]
917+
fn exif_iter_tz(path: &str, tz: &str, time: &str, img_type: MimeImage) {
906918
let buf = read_sample(path).unwrap();
907919
let (data, _) = extract_exif_with_mime(img_type, &buf, None).unwrap();
908920
let subslice_in_range = data.and_then(|x| buf.subslice_in_range(x)).unwrap();
909921
let iter = input_into_iter((buf, subslice_in_range), None).unwrap();
910-
let expect = if tz.is_empty() {
922+
let expect = if tz == "-" {
911923
None
912924
} else {
913925
Some(tz.to_string())
914926
};
915927
assert_eq!(iter.tz, expect);
928+
let exif: Exif = iter.into();
929+
let value = exif.get(crate::ExifTag::DateTimeOriginal);
930+
if time == "-" {
931+
assert!(value.is_none());
932+
} else {
933+
let value = value.unwrap();
934+
assert_eq!(value.to_string(), time);
935+
}
916936
}
917937
}

src/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ mod tests {
524524
#[case("embedded-in-heic.mov", Track)]
525525
#[case("exif.heic", Exif)]
526526
#[case("exif.jpg", Exif)]
527+
#[case("exif-no-tz.jpg", Exif)]
527528
#[case("fujifilm_x_t1_01.raf.meta", Exif)]
528529
#[case("meta.mov", Track)]
529530
#[case("meta.mp4", Track)]

src/values.rs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use std::{fmt::Display, string::FromUtf8Error};
22

3-
use chrono::{
4-
offset::LocalResult, DateTime, FixedOffset, Local, NaiveDateTime, Offset, TimeZone as _, Utc,
5-
};
3+
use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, Utc};
64

75
use nom::{multi::many_m_n, number::Endianness, AsChar};
86
#[cfg(feature = "json_dump")]
@@ -13,6 +11,7 @@ use crate::ExifTag;
1311

1412
/// Represent a parsed entry value.
1513
#[derive(Debug, Clone, PartialEq)]
14+
#[non_exhaustive]
1615
pub enum EntryValue {
1716
Text(String),
1817
URational(URational),
@@ -32,6 +31,7 @@ pub enum EntryValue {
3231
F64(f64),
3332

3433
Time(DateTime<FixedOffset>),
34+
NaiveDateTime(NaiveDateTime),
3535
Undefined(Vec<u8>),
3636

3737
URationalArray(Vec<URational>),
@@ -141,18 +141,14 @@ impl EntryValue {
141141
let s = get_cstr(data).map_err(|e| Error::InvalidData(e.to_string()))?;
142142

143143
let t = if let Some(tz) = tz {
144-
let s = format!("{s} {tz}");
145-
DateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S %z")?
144+
let tz = repair_tz_str(tz);
145+
let ss = format!("{s} {tz}");
146+
match DateTime::parse_from_str(&ss, "%Y:%m:%d %H:%M:%S %z") {
147+
Ok(t) => t,
148+
Err(_) => return Ok(EntryValue::NaiveDateTime(parse_naive_time(s)?)),
149+
}
146150
} else {
147-
let t = NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S")?;
148-
let t = Local.from_local_datetime(&t);
149-
let t = if let LocalResult::Single(t) = t {
150-
Ok(t)
151-
} else {
152-
Err(Error::InvalidData(format!("parse time failed: {s}")))
153-
}?;
154-
155-
t.with_timezone(t.offset())
151+
return Ok(EntryValue::NaiveDateTime(parse_naive_time(s)?));
156152
};
157153

158154
return Ok(EntryValue::Time(t));
@@ -370,6 +366,30 @@ impl EntryValue {
370366
}
371367
}
372368
}
369+
fn parse_naive_time(s: String) -> Result<NaiveDateTime, ParseEntryError> {
370+
let t = NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S")?;
371+
Ok(t)
372+
}
373+
// fn parse_time_with_local_tz(s: String) -> Result<DateTime<FixedOffset>, ParseEntryError> {
374+
// let t = NaiveDateTime::parse_from_str(&s, "%Y:%m:%d %H:%M:%S")?;
375+
// let t = Local.from_local_datetime(&t);
376+
// let t = if let LocalResult::Single(t) = t {
377+
// Ok(t)
378+
// } else {
379+
// Err(Error::InvalidData(format!("parse time failed: {s}")))
380+
// }?;
381+
// Ok(t.with_timezone(t.offset()))
382+
// }
383+
384+
fn repair_tz_str(tz: &str) -> String {
385+
if let Some(idx) = tz.find(":") {
386+
if tz[idx..].len() < 3 {
387+
// Add tailed 0
388+
return format!("{tz}0");
389+
}
390+
}
391+
tz.into()
392+
}
373393

374394
/// # Exif Data format
375395
///
@@ -457,6 +477,7 @@ impl Display for EntryValue {
457477
EntryValue::U8(v) => Display::fmt(&v, f),
458478
EntryValue::I8(v) => Display::fmt(&v, f),
459479
EntryValue::Time(v) => Display::fmt(&v.to_rfc3339(), f),
480+
EntryValue::NaiveDateTime(v) => Display::fmt(&v.format("%Y-%m-%d %H:%M:%S"), f),
460481
EntryValue::Undefined(v) => {
461482
// Display up to MAX_DISPLAY_NUM components, and replace the rest with ellipsis
462483
const MAX_DISPLAY_NUM: usize = 8;

testdata/exif-no-tz.jpg

17.4 KB
Loading

0 commit comments

Comments
 (0)