Date a DNG whose IFDs follow its pixels: read the head and the tail
The scan reads the first 256 KB of a file for its metadata. A camera writes its IFDs at the front, so that is the whole structure; the linear DNG a merge writes puts its first IFD after the pixels, and rawler, given the head alone, finds no decoder in it. The composite was catalogued without a date and sorted to the very end of the grid, after every dated photograph — which is where a panorama merged on the tablet went unfound. dr-decode's own TIFF reader now reads through a head and a tail at a known offset; trailing_ifd says where the tail starts and metadata_split reads the two together. The scan, when the head fails and points beyond itself, fetches from the IFD to the end — kilobytes — and dates the file from both. Tested against the writer's own output.
This commit is contained in:
@@ -304,6 +304,26 @@ pub fn metadata(bytes: &[u8]) -> Result<Metadata, DecodeError> {
|
|||||||
error::guarded("metadata", || metadata_unguarded(bytes))
|
error::guarded("metadata", || metadata_unguarded(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-CAT-5
|
||||||
|
/// Where a TIFF-shaped file keeps its first IFD, when the head handed to
|
||||||
|
/// [`metadata`] does not reach it — the linear DNG a merge writes puts its
|
||||||
|
/// IFDs after the pixels, and rawler, given the head alone, finds no
|
||||||
|
/// decoder in it. The caller fetches from this offset to the end and
|
||||||
|
/// reads the two ranges with [`metadata_split`].
|
||||||
|
pub fn trailing_ifd(head: &[u8]) -> Option<u64> {
|
||||||
|
locate::trailing_ifd(head)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-CAT-5
|
||||||
|
/// [`metadata`] for a file read in two ranges: `head` from offset 0 and
|
||||||
|
/// `tail` from `tail_at`. The EXIF sub-IFD such a file wrote before its
|
||||||
|
/// pixels is in the head; the first IFD and its values are in the tail.
|
||||||
|
pub fn metadata_split(head: &[u8], tail: &[u8], tail_at: u64) -> Result<Metadata, DecodeError> {
|
||||||
|
error::guarded("metadata", || {
|
||||||
|
locate::tiff_metadata_split(head, tail, tail_at)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn metadata_unguarded(bytes: &[u8]) -> Result<Metadata, DecodeError> {
|
fn metadata_unguarded(bytes: &[u8]) -> Result<Metadata, DecodeError> {
|
||||||
use rawler::rawsource::RawSource;
|
use rawler::rawsource::RawSource;
|
||||||
|
|
||||||
|
|||||||
@@ -183,22 +183,65 @@ struct Entry {
|
|||||||
value: u32,
|
value: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bytes a [`TiffReader`] reads: the head of a file, and optionally a
|
||||||
|
/// second range from further in, at a known offset.
|
||||||
|
///
|
||||||
|
/// A camera writes its IFDs at the front, so the first 256 KB of a file
|
||||||
|
/// is the whole structure. A file written strip by strip — the linear
|
||||||
|
/// DNG a merge produces — has its first IFD at the *end*, after the
|
||||||
|
/// pixels, and a reader that only has the head sees a pointer into
|
||||||
|
/// nothing. Rather than fetch 800 MB to read a date, the caller fetches
|
||||||
|
/// the head, asks [`crate::trailing_ifd`] where the IFD is, fetches that
|
||||||
|
/// tail, and reads through both. Offsets are the file's own throughout;
|
||||||
|
/// a read that falls in neither range is simply absent.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Src<'a> {
|
||||||
|
head: &'a [u8],
|
||||||
|
tail: &'a [u8],
|
||||||
|
/// Where `tail` starts in the file.
|
||||||
|
tail_at: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Src<'a> {
|
||||||
|
fn whole(data: &'a [u8]) -> Self {
|
||||||
|
Src {
|
||||||
|
head: data,
|
||||||
|
tail: &[],
|
||||||
|
tail_at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, start: usize, len: usize) -> Option<&'a [u8]> {
|
||||||
|
let end = start.checked_add(len)?;
|
||||||
|
if let Some(b) = self.head.get(start..end) {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let s = start.checked_sub(self.tail_at)?;
|
||||||
|
self.tail.get(s..s.checked_add(len)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A minimal TIFF structure reader.
|
/// A minimal TIFF structure reader.
|
||||||
///
|
///
|
||||||
/// Deliberately not a general TIFF parser: it reads the IFD chain and entry
|
/// Deliberately not a general TIFF parser: it reads the IFD chain and entry
|
||||||
/// values and nothing else, because that is all locating a preview needs.
|
/// values and nothing else, because that is all locating a preview needs.
|
||||||
struct TiffReader<'a> {
|
struct TiffReader<'a> {
|
||||||
data: &'a [u8],
|
data: Src<'a>,
|
||||||
little_endian: bool,
|
little_endian: bool,
|
||||||
first_ifd: u32,
|
first_ifd: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TiffReader<'a> {
|
impl<'a> TiffReader<'a> {
|
||||||
fn new(data: &'a [u8]) -> Option<Self> {
|
fn new(data: &'a [u8]) -> Option<Self> {
|
||||||
if data.len() < 8 {
|
Self::over(Src::whole(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn over(data: Src<'a>) -> Option<Self> {
|
||||||
|
let head = data.head;
|
||||||
|
if head.len() < 8 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let little_endian = match &data[0..2] {
|
let little_endian = match &head[0..2] {
|
||||||
b"II" => true,
|
b"II" => true,
|
||||||
b"MM" => false,
|
b"MM" => false,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
@@ -362,9 +405,7 @@ impl<'a> TiffReader<'a> {
|
|||||||
};
|
};
|
||||||
raw[..len.min(4)].to_vec()
|
raw[..len.min(4)].to_vec()
|
||||||
} else {
|
} else {
|
||||||
self.data
|
self.data.get(e.value as usize, len)?.to_vec()
|
||||||
.get(e.value as usize..e.value as usize + len)?
|
|
||||||
.to_vec()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let s = String::from_utf8_lossy(&bytes);
|
let s = String::from_utf8_lossy(&bytes);
|
||||||
@@ -396,8 +437,7 @@ impl<'a> TiffReader<'a> {
|
|||||||
// same way to recover the original byte order.
|
// same way to recover the original byte order.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let start = e.value as usize;
|
self.data.get(e.value as usize, len)
|
||||||
self.data.get(start..start.checked_add(len)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn offsets(&self, e: &Entry) -> Vec<u32> {
|
fn offsets(&self, e: &Entry) -> Vec<u32> {
|
||||||
@@ -420,8 +460,8 @@ impl<'a> TiffReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_u16(data: &[u8], at: usize, le: bool) -> Option<u16> {
|
fn read_u16(data: Src<'_>, at: usize, le: bool) -> Option<u16> {
|
||||||
let b = data.get(at..at + 2)?;
|
let b = data.get(at, 2)?;
|
||||||
Some(if le {
|
Some(if le {
|
||||||
u16::from_le_bytes([b[0], b[1]])
|
u16::from_le_bytes([b[0], b[1]])
|
||||||
} else {
|
} else {
|
||||||
@@ -429,8 +469,8 @@ fn read_u16(data: &[u8], at: usize, le: bool) -> Option<u16> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_u32(data: &[u8], at: usize, le: bool) -> Option<u32> {
|
fn read_u32(data: Src<'_>, at: usize, le: bool) -> Option<u32> {
|
||||||
let b = data.get(at..at + 4)?;
|
let b = data.get(at, 4)?;
|
||||||
Some(if le {
|
Some(if le {
|
||||||
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
|
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
|
||||||
} else {
|
} else {
|
||||||
@@ -460,7 +500,36 @@ pub fn jpeg_metadata(bytes: &[u8]) -> Result<crate::Metadata, crate::DecodeError
|
|||||||
/// no `DateTimeOriginal` for some DNGs whose tag sits plainly at byte 826 —
|
/// no `DateTimeOriginal` for some DNGs whose tag sits plainly at byte 826 —
|
||||||
/// and without this fallback those images are silently undated.
|
/// and without this fallback those images are silently undated.
|
||||||
pub fn tiff_metadata(tiff_data: &[u8]) -> Result<crate::Metadata, crate::DecodeError> {
|
pub fn tiff_metadata(tiff_data: &[u8]) -> Result<crate::Metadata, crate::DecodeError> {
|
||||||
let reader = TiffReader::new(tiff_data)
|
tiff_metadata_over(Src::whole(tiff_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a TIFF-shaped file's first IFD is, when the head does not reach
|
||||||
|
/// it: the offset to fetch from, so [`tiff_metadata_split`] can read it.
|
||||||
|
/// `None` for a file that is not TIFF, or whose IFD the head already holds.
|
||||||
|
pub fn trailing_ifd(head: &[u8]) -> Option<u64> {
|
||||||
|
let r = TiffReader::new(head)?;
|
||||||
|
let at = r.first_ifd as u64;
|
||||||
|
(at >= head.len() as u64).then_some(at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`tiff_metadata`] over a head and a tail fetched separately: the head
|
||||||
|
/// from offset 0, the tail from `tail_at`. For the file whose IFDs follow
|
||||||
|
/// its pixels.
|
||||||
|
pub fn tiff_metadata_split(
|
||||||
|
head: &[u8],
|
||||||
|
tail: &[u8],
|
||||||
|
tail_at: u64,
|
||||||
|
) -> Result<crate::Metadata, crate::DecodeError> {
|
||||||
|
tiff_metadata_over(Src {
|
||||||
|
head,
|
||||||
|
tail,
|
||||||
|
tail_at: usize::try_from(tail_at)
|
||||||
|
.map_err(|_| crate::DecodeError::Metadata("tail offset out of range".into()))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tiff_metadata_over(src: Src<'_>) -> Result<crate::Metadata, crate::DecodeError> {
|
||||||
|
let reader = TiffReader::over(src)
|
||||||
.ok_or_else(|| crate::DecodeError::Metadata("malformed EXIF header".into()))?;
|
.ok_or_else(|| crate::DecodeError::Metadata("malformed EXIF header".into()))?;
|
||||||
|
|
||||||
let mut md = crate::Metadata::default();
|
let mut md = crate::Metadata::default();
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ mod tests {
|
|||||||
let source = SourceMetadata {
|
let source = SourceMetadata {
|
||||||
make: Some("Canon".into()),
|
make: Some("Canon".into()),
|
||||||
model: Some("Canon EOS 6D".into()),
|
model: Some("Canon EOS 6D".into()),
|
||||||
|
captured_at: Some(1_754_398_664),
|
||||||
|
captured_offset: Some(120),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
write_linear_dng(
|
write_linear_dng(
|
||||||
@@ -301,6 +303,32 @@ mod tests {
|
|||||||
assert_eq!((crop.p.x, crop.p.y, crop.d.w, crop.d.h), (2, 1, 15, 10));
|
assert_eq!((crop.p.x, crop.p.y, crop.d.w, crop.d.h), (2, 1, 15, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_catalog_reads_the_date_from_a_head_and_a_tail() {
|
||||||
|
// TRACES: FR-CAT-5
|
||||||
|
// The IFDs follow the pixels, so a scan that has the first bytes of
|
||||||
|
// the file has a pointer into nothing; rawler finds no decoder in
|
||||||
|
// that, and the composite would sit undated at the end of the grid.
|
||||||
|
// The scan's second range — from the first IFD to the end — with
|
||||||
|
// the head is enough to date it, and to name the camera.
|
||||||
|
let bytes = write(640, 400, 64);
|
||||||
|
let head = &bytes[..4096];
|
||||||
|
assert!(
|
||||||
|
dr_decode::metadata(head).is_err(),
|
||||||
|
"the head alone must not read"
|
||||||
|
);
|
||||||
|
let at = dr_decode::trailing_ifd(head).expect("the IFD is beyond the head");
|
||||||
|
assert!(at as usize > head.len());
|
||||||
|
let tail = &bytes[at as usize..];
|
||||||
|
assert!(tail.len() < 4096, "the tail is the IFD, not the pixels");
|
||||||
|
let md = dr_decode::metadata_split(head, tail, at).expect("read from two ranges");
|
||||||
|
assert_eq!(md.captured_at, Some(1_754_398_664));
|
||||||
|
assert_eq!(md.captured_offset, Some(120));
|
||||||
|
assert_eq!(md.model.as_deref(), Some("Canon EOS 6D"));
|
||||||
|
// A head that holds everything is not a trailing-IFD file.
|
||||||
|
assert_eq!(dr_decode::trailing_ifd(&bytes), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_strip_of_the_wrong_length_is_refused() {
|
fn a_strip_of_the_wrong_length_is_refused() {
|
||||||
let mut bytes = std::io::Cursor::new(Vec::new());
|
let mut bytes = std::io::Cursor::new(Vec::new());
|
||||||
|
|||||||
+29
-29
File diff suppressed because one or more lines are too long
+34
-5
@@ -3230,7 +3230,7 @@ async fn fetch_preview(
|
|||||||
// The same bytes carry EXIF. Reading it here is free — the alternative is
|
// The same bytes carry EXIF. Reading it here is free — the alternative is
|
||||||
// a second 256 KB fetch per image over the whole library.
|
// a second 256 KB fetch per image over the whole library.
|
||||||
if req.needs_metadata {
|
if req.needs_metadata {
|
||||||
collect_metadata(&header, req, found_metadata);
|
collect_metadata(backend, &id, &header, req, found_metadata).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read unconditionally, unlike the rest of the EXIF above: `needs_metadata`
|
// Read unconditionally, unlike the rest of the EXIF above: `needs_metadata`
|
||||||
@@ -3350,9 +3350,38 @@ pub(crate) fn store_thumbnail(
|
|||||||
///
|
///
|
||||||
/// Shared by both paths — the thumbnail fetch, which gets the header anyway,
|
/// Shared by both paths — the thumbnail fetch, which gets the header anyway,
|
||||||
/// and the header-only pass for images whose pixels were already cached.
|
/// and the header-only pass for images whose pixels were already cached.
|
||||||
fn collect_metadata(header: &[u8], req: &ThumbnailRequest, out: &mut Vec<MetadataFound>) {
|
async fn collect_metadata(
|
||||||
let Ok(md) = dr_decode::metadata(header) else {
|
backend: &dyn RemoteBackend,
|
||||||
return;
|
id: &RemoteId,
|
||||||
|
header: &[u8],
|
||||||
|
req: &ThumbnailRequest,
|
||||||
|
out: &mut Vec<MetadataFound>,
|
||||||
|
) {
|
||||||
|
let md = match dr_decode::metadata(header) {
|
||||||
|
Ok(md) => md,
|
||||||
|
Err(first) => {
|
||||||
|
// A file whose IFDs follow its pixels — the linear DNG a merge
|
||||||
|
// writes — has nothing for the decoder in its head but a
|
||||||
|
// pointer. Its structure is a few kilobytes at the end; fetch
|
||||||
|
// that and read the two ranges together, rather than leave the
|
||||||
|
// composite undated at the end of the grid.
|
||||||
|
let Some(at) = dr_decode::trailing_ifd(header).filter(|at| *at < req.size) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match backend.get(id, Some(at..req.size)).await {
|
||||||
|
Ok(tail) => match dr_decode::metadata_split(header, &tail, at) {
|
||||||
|
Ok(md) => md,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("metadata: {}: head {first}; head and tail {e}", req.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("metadata: {}: tail not fetched: {e}", req.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
out.push(MetadataFound {
|
out.push(MetadataFound {
|
||||||
image_id: req.image_id,
|
image_id: req.image_id,
|
||||||
@@ -3442,7 +3471,7 @@ async fn read_metadata_only(
|
|||||||
for attempt in 1..=ATTEMPTS {
|
for attempt in 1..=ATTEMPTS {
|
||||||
match backend.get(&id, Some(0..dr_decode::HEADER_BYTES)).await {
|
match backend.get(&id, Some(0..dr_decode::HEADER_BYTES)).await {
|
||||||
Ok(header) => {
|
Ok(header) => {
|
||||||
collect_metadata(&header, req, found);
|
collect_metadata(backend, &id, &header, req, found).await;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Err(e) if e.is_transient() && attempt < ATTEMPTS => {
|
Err(e) if e.is_transient() && attempt < ATTEMPTS => {
|
||||||
|
|||||||
Reference in New Issue
Block a user