Merge pull request 'fix(dates): resolve IFD0 ModifyDate as a last-resort capture date' (#181) from fix/modifydate-fallback into master
Reviewed-on: #181
This commit was merged in pull request #181.
This commit is contained in:
commit
a605ccd6f8
1 file changed
+65
@@ -322,6 +322,13 @@ pub fn resolve_dates_batch(
|
|||||||
/// timestamps. `ContentCreateDate` is Apple's iOS-set tag; it often
|
/// timestamps. `ContentCreateDate` is Apple's iOS-set tag; it often
|
||||||
/// reflects local capture time on iPhone exports better than the others.
|
/// reflects local capture time on iPhone exports better than the others.
|
||||||
///
|
///
|
||||||
|
/// `ModifyDate` comes last and only from `IFD0` (see
|
||||||
|
/// [`MODIFY_DATE_GROUP`]): that's EXIF `DateTime`, which the kamadak
|
||||||
|
/// ingest path already takes as the capture date when nothing better
|
||||||
|
/// exists. Scans, edits and some downloads carry only that tag, and
|
||||||
|
/// without it here the drain resolved them to fs_time and then refused
|
||||||
|
/// to overwrite their tag-sourced legacy date — stuck forever.
|
||||||
|
///
|
||||||
/// Notably absent: `FileModifyDate` / `FileAccessDate` — those are
|
/// Notably absent: `FileModifyDate` / `FileAccessDate` — those are
|
||||||
/// filesystem-derived and the resolver covers them via the `fs_time`
|
/// filesystem-derived and the resolver covers them via the `fs_time`
|
||||||
/// fallback. Letting exiftool pull them here would mask "no real EXIF
|
/// fallback. Letting exiftool pull them here would mask "no real EXIF
|
||||||
@@ -333,8 +340,14 @@ const EXIFTOOL_DATE_TAGS: &[&str] = &[
|
|||||||
"MediaCreateDate",
|
"MediaCreateDate",
|
||||||
"TrackCreateDate",
|
"TrackCreateDate",
|
||||||
"ContentCreateDate",
|
"ContentCreateDate",
|
||||||
|
"ModifyDate",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The only group whose `ModifyDate` counts as a date: `IFD0`, i.e. EXIF
|
||||||
|
/// `DateTime`. XMP / QuickTime `ModifyDate` record when an editor or
|
||||||
|
/// muxer last touched the file, which isn't a capture time.
|
||||||
|
const MODIFY_DATE_GROUP: &str = "IFD0";
|
||||||
|
|
||||||
/// Offset + GPS evidence tags requested in the same exiftool pass. They
|
/// Offset + GPS evidence tags requested in the same exiftool pass. They
|
||||||
/// feed the camera-offset cascade for naive wall-clock readings that come
|
/// feed the camera-offset cascade for naive wall-clock readings that come
|
||||||
/// from photo-EXIF groups (ADR-0001).
|
/// from photo-EXIF groups (ADR-0001).
|
||||||
@@ -735,6 +748,9 @@ fn resolve_exiftool_payload(
|
|||||||
});
|
});
|
||||||
|
|
||||||
for cand in candidates {
|
for cand in candidates {
|
||||||
|
if cand.tag == "ModifyDate" && cand.group != MODIFY_DATE_GROUP {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(reading) = parse_exiftool_date_string(&cand.value) else {
|
let Some(reading) = parse_exiftool_date_string(&cand.value) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -1063,6 +1079,55 @@ mod tests {
|
|||||||
assert_eq!(resolved.date_utc, Some(1_720_108_800));
|
assert_eq!(resolved.date_utc, Some(1_720_108_800));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ifd0_modify_date_is_a_last_resort() {
|
||||||
|
// Scans / edits with only EXIF DateTime (exiftool: IFD0:ModifyDate)
|
||||||
|
// resolve from it as a camera wall clock, like the kamadak path.
|
||||||
|
let resolved = resolve_exiftool_payload(
|
||||||
|
&tags(&[
|
||||||
|
("IFD0", "ModifyDate", "2009:03:04 14:55:48"),
|
||||||
|
("ExifIFD", "DateTimeOriginal", ""),
|
||||||
|
]),
|
||||||
|
&ExiftoolEvidence {
|
||||||
|
offset_time: Some(-300),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved.source, DateSource::Exiftool);
|
||||||
|
assert_eq!(resolved.date_utc, Some(1_236_196_548));
|
||||||
|
assert_eq!(resolved.camera_offset_minutes, Some(-300));
|
||||||
|
|
||||||
|
// Any capture tag outranks it.
|
||||||
|
let resolved = resolve_exiftool_payload(
|
||||||
|
&tags(&[
|
||||||
|
("IFD0", "ModifyDate", "2022:11:08 15:18:20"),
|
||||||
|
("ExifIFD", "CreateDate", "2022:11:01 09:00:00"),
|
||||||
|
]),
|
||||||
|
&ExiftoolEvidence {
|
||||||
|
offset_time: Some(-240),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved.date_utc, Some(1_667_307_600));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_exif_modify_date_is_ignored() {
|
||||||
|
// XMP / QuickTime ModifyDate is an edit or mux time, not capture.
|
||||||
|
for group in ["XMP-xmp", "QuickTime"] {
|
||||||
|
assert!(
|
||||||
|
resolve_exiftool_payload(
|
||||||
|
&tags(&[(group, "ModifyDate", "2024:01:01 00:00:00")]),
|
||||||
|
&ExiftoolEvidence::default(),
|
||||||
|
)
|
||||||
|
.is_none(),
|
||||||
|
"{group}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_date_falls_through_to_next_tag() {
|
fn zero_date_falls_through_to_next_tag() {
|
||||||
// exiftool emits "0000:00:00 00:00:00" for missing-but-allocated
|
// exiftool emits "0000:00:00 00:00:00" for missing-but-allocated
|
||||||
|
|||||||
Reference in new issue
Block a user