Add a dedicated Directory type (#277)

This commit is contained in:
Aurelia Molzer authored and GitHub committed 2025-07-07 19:59:37 -07:00
1 parent 816fb85304
commit df4e42eecc
7 files changed
+239 -43

No files matched your search

+6
View File
@@ -333,6 +333,10 @@ impl Entry {
} }
} }
pub fn count(&self) -> u64 {
self.count
}
/// Returns a mem_reader for the offset/value field /// Returns a mem_reader for the offset/value field
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> { fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order) EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
@@ -668,4 +672,6 @@ fn offset_to_sbytes(n: usize, entry: &Entry) -> TiffResult<Value> {
} }
/// Type representing an Image File Directory /// Type representing an Image File Directory
#[doc(hidden)]
#[deprecated = "Use struct `tiff::Directory` instead which contains all fields relevant to an Image File Directory, including the offset to the next directory"]
pub type Directory = HashMap<Tag, Entry>; pub type Directory = HashMap<Tag, Entry>;
+10 -8
View File
@@ -1,4 +1,4 @@
use super::ifd::{Directory, Value}; use super::ifd::Value;
use super::stream::{ByteOrder, PackBitsReader}; use super::stream::{ByteOrder, PackBitsReader};
use super::tag_reader::TagReader; use super::tag_reader::TagReader;
use super::{predict_f16, predict_f32, predict_f64, Limits}; use super::{predict_f16, predict_f32, predict_f64, Limits};
@@ -6,7 +6,9 @@ use super::{stream::EndianReader, ChunkType};
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
}; };
use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{self, Cursor, Read, Seek}; use std::io::{self, Cursor, Read, Seek};
use std::sync::Arc; use std::sync::Arc;
@@ -115,7 +117,7 @@ impl Image {
}; };
let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG
&& ifd.contains_key(&Tag::JPEGTables) && ifd.contains(Tag::JPEGTables)
{ {
let vec = tag_reader let vec = tag_reader
.find_tag(Tag::JPEGTables)? .find_tag(Tag::JPEGTables)?
@@ -210,10 +212,10 @@ impl Image {
let strip_decoder; let strip_decoder;
let tile_attributes; let tile_attributes;
match ( match (
ifd.contains_key(&Tag::StripByteCounts), ifd.contains(Tag::StripByteCounts),
ifd.contains_key(&Tag::StripOffsets), ifd.contains(Tag::StripOffsets),
ifd.contains_key(&Tag::TileByteCounts), ifd.contains(Tag::TileByteCounts),
ifd.contains_key(&Tag::TileOffsets), ifd.contains(Tag::TileOffsets),
) { ) {
(true, true, false, false) => { (true, true, false, false) => {
chunk_type = ChunkType::Strip; chunk_type = ChunkType::Strip;
@@ -676,7 +678,7 @@ impl Image {
} }
} }
} else { } else {
for (i, row) in buf for (_i, row) in buf
.chunks_mut(output_row_stride) .chunks_mut(output_row_stride)
.take(data_dims.1 as usize) .take(data_dims.1 as usize)
.enumerate() .enumerate()
+37 -31
View File
@@ -1,16 +1,16 @@
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::io::{self, Read, Seek}; use std::io::{self, Read, Seek};
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
Tag, Type, SampleFormat, Tag, Type,
}; };
use crate::{ use crate::{
bytecast, ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError, bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
UsageError,
}; };
use half::f16; use half::f16;
use self::ifd::Directory;
use self::image::Image; use self::image::Image;
use self::stream::{ByteOrder, EndianReader}; use self::stream::{ByteOrder, EndianReader};
@@ -263,9 +263,9 @@ where
reader: EndianReader<R>, reader: EndianReader<R>,
bigtiff: bool, bigtiff: bool,
limits: Limits, limits: Limits,
next_ifd: Option<u64>, next_ifd: Option<IfdPointer>,
ifd_offsets: Vec<u64>, ifd_offsets: Vec<IfdPointer>,
seen_ifds: HashSet<u64>, seen_ifds: HashSet<IfdPointer>,
image: Image, image: Image,
} }
@@ -487,11 +487,13 @@ impl<R: Read + Seek> Decoder<R> {
)) ))
} }
}; };
let next_ifd = if bigtiff { let next_ifd = if bigtiff {
Some(reader.read_u64()?) Some(reader.read_u64()?)
} else { } else {
Some(u64::from(reader.read_u32()?)) Some(u64::from(reader.read_u32()?))
}; }
.map(IfdPointer);
let mut seen_ifds = HashSet::new(); let mut seen_ifds = HashSet::new();
seen_ifds.insert(*next_ifd.as_ref().unwrap()); seen_ifds.insert(*next_ifd.as_ref().unwrap());
@@ -557,9 +559,9 @@ impl<R: Read + Seek> Decoder<R> {
loop { loop {
// Follow the list until we find the one we want, or we reach the end, whichever happens first // Follow the list until we find the one we want, or we reach the end, whichever happens first
let (_ifd, next_ifd) = self.next_ifd()?; let ifd = self.next_ifd()?;
if next_ifd.is_none() { if ifd.next().is_none() {
break; break;
} }
@@ -571,8 +573,7 @@ impl<R: Read + Seek> Decoder<R> {
// If the index is within the list of ifds then we can load the selected image/IFD // If the index is within the list of ifds then we can load the selected image/IFD
if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) { if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) {
let (ifd, _next_ifd) = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?; let ifd = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?;
self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?; self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?;
Ok(()) Ok(())
@@ -583,20 +584,20 @@ impl<R: Read + Seek> Decoder<R> {
} }
} }
fn next_ifd(&mut self) -> TiffResult<(Directory, Option<u64>)> { fn next_ifd(&mut self) -> TiffResult<Directory> {
if self.next_ifd.is_none() { if self.next_ifd.is_none() {
return Err(TiffError::FormatError( return Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound, TiffFormatError::ImageFileDirectoryNotFound,
)); ));
} }
let (ifd, next_ifd) = Self::read_ifd( let ifd = Self::read_ifd(
&mut self.reader, &mut self.reader,
self.bigtiff, self.bigtiff,
self.next_ifd.take().unwrap(), self.next_ifd.take().unwrap(),
)?; )?;
if let Some(next) = next_ifd { if let Some(next) = ifd.next() {
if !self.seen_ifds.insert(next) { if !self.seen_ifds.insert(next) {
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)); return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
} }
@@ -604,15 +605,14 @@ impl<R: Read + Seek> Decoder<R> {
self.ifd_offsets.push(next); self.ifd_offsets.push(next);
} }
Ok((ifd, next_ifd)) Ok(ifd)
} }
/// Reads in the next image. /// Reads in the next image.
/// If there is no further image in the TIFF file a format error is returned. /// If there is no further image in the TIFF file a format error is returned.
/// To determine whether there are more images call `TIFFDecoder::more_images` instead. /// To determine whether there are more images call `TIFFDecoder::more_images` instead.
pub fn next_image(&mut self) -> TiffResult<()> { pub fn next_image(&mut self) -> TiffResult<()> {
let (ifd, _next_ifd) = self.next_ifd()?; let ifd = self.next_ifd()?;
self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?; self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?;
Ok(()) Ok(())
} }
@@ -734,6 +734,13 @@ impl<R: Read + Seek> Decoder<R> {
self.reader.goto_offset(offset) self.reader.goto_offset(offset)
} }
/// Read a directory from a known offset.
///
/// This may always modify the position of the reader.
pub fn read_directory(&mut self, ptr: IfdPointer) -> TiffResult<Directory> {
Self::read_ifd(&mut self.reader, self.bigtiff, ptr)
}
/// Reads a IFD entry. /// Reads a IFD entry.
// An IFD entry has four fields: // An IFD entry has four fields:
// //
@@ -775,17 +782,18 @@ impl<R: Read + Seek> Decoder<R> {
fn read_ifd( fn read_ifd(
reader: &mut EndianReader<R>, reader: &mut EndianReader<R>,
bigtiff: bool, bigtiff: bool,
ifd_location: u64, ifd_location: IfdPointer,
) -> TiffResult<(Directory, Option<u64>)> { ) -> TiffResult<Directory> {
reader.goto_offset(ifd_location)?; reader.goto_offset(ifd_location.0)?;
let mut dir: Directory = HashMap::new(); let mut entries: BTreeMap<_, _> = BTreeMap::new();
let num_tags = if bigtiff { let num_tags = if bigtiff {
reader.read_u64()? reader.read_u64()?
} else { } else {
reader.read_u16()?.into() reader.read_u16()?.into()
}; };
for _ in 0..num_tags { for _ in 0..num_tags {
let (tag, entry) = match Self::read_entry(reader, bigtiff)? { let (tag, entry) = match Self::read_entry(reader, bigtiff)? {
Some(val) => val, Some(val) => val,
@@ -793,7 +801,8 @@ impl<R: Read + Seek> Decoder<R> {
continue; continue;
} // Unknown data type in tag, skip } // Unknown data type in tag, skip
}; };
dir.insert(tag, entry);
entries.insert(tag.to_u16(), entry);
} }
let next_ifd = if bigtiff { let next_ifd = if bigtiff {
@@ -802,18 +811,15 @@ impl<R: Read + Seek> Decoder<R> {
reader.read_u32()?.into() reader.read_u32()?.into()
}; };
let next_ifd = match next_ifd { let next_ifd = core::num::NonZeroU64::new(next_ifd);
0 => None,
_ => Some(next_ifd),
};
Ok((dir, next_ifd)) Ok(Directory { entries, next_ifd })
} }
/// Tries to retrieve a tag. /// Tries to retrieve a tag.
/// Return `Ok(None)` if the tag is not present. /// Return `Ok(None)` if the tag is not present.
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> { pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
let entry = match self.image().ifd.as_ref().unwrap().get(&tag) { let entry = match self.image().ifd.as_ref().unwrap().get(tag) {
None => return Ok(None), None => return Ok(None),
Some(entry) => entry.clone(), Some(entry) => entry.clone(),
}; };
@@ -928,7 +934,7 @@ impl<R: Read + Seek> Decoder<R> {
self.image.ifd.as_ref().unwrap().iter().map(|(tag, entry)| { self.image.ifd.as_ref().unwrap().iter().map(|(tag, entry)| {
entry entry
.val(&self.limits, self.bigtiff, &mut self.reader) .val(&self.limits, self.bigtiff, &mut self.reader)
.map(|value| (*tag, value)) .map(|value| (tag, value))
}) })
} }
+3 -3
View File
@@ -1,9 +1,9 @@
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use crate::tags::Tag; use crate::{tags::Tag, Directory};
use crate::{TiffError, TiffFormatError, TiffResult}; use crate::{TiffError, TiffFormatError, TiffResult};
use super::ifd::{Directory, Value}; use super::ifd::Value;
use super::stream::EndianReader; use super::stream::EndianReader;
use super::Limits; use super::Limits;
@@ -15,7 +15,7 @@ pub(crate) struct TagReader<'a, R: Read + Seek> {
} }
impl<'a, R: Read + Seek> TagReader<'a, R> { impl<'a, R: Read + Seek> TagReader<'a, R> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> { pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
Ok(match self.ifd.get(&tag) { Ok(match self.ifd.get(tag) {
Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?), Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?),
None => None, None => None,
}) })
+161
View File
@@ -0,0 +1,161 @@
use core::fmt;
use std::{collections::BTreeMap, num::NonZeroU64};
use crate::{
decoder::ifd::Entry,
tags::{IfdPointer, Tag},
};
/// An Image File Directory (IFD).
#[doc(alias = "IFD")]
pub struct Directory {
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
/// does not mind unordered entries).
pub(crate) entries: BTreeMap<u16, Entry>,
pub(crate) next_ifd: Option<NonZeroU64>,
}
impl Directory {
pub fn empty(next: IfdPointer) -> Self {
Directory {
entries: BTreeMap::new(),
next_ifd: NonZeroU64::new(next.0),
}
}
/// Retrieve the value associated with a tag.
pub fn get(&self, tag: Tag) -> Option<&Entry> {
self.entries.get(&tag.to_u16())
}
/// Check if the directory contains a specified tag.
pub fn contains(&self, tag: Tag) -> bool {
self.entries.contains_key(&tag.to_u16())
}
/// Iterate over all known and unknown tags in this directory.
pub fn iter(&self) -> impl Iterator<Item = (Tag, &Entry)> + '_ {
self.entries
.iter()
.map(|(k, v)| (Tag::from_u16_exhaustive(*k), v))
}
/// Insert additional entries into the directory.
///
/// Note that a directory can contain at most `u16::MAX` values. There may be one entry that
/// does not fit into the directory. This entry is silently ignored (please check [`Self::len`]
/// to detect the condition). Providing a tag multiple times or a tag that already exists
/// within this directory overwrites the entry.
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Tag, Entry)>) {
// Code size conscious extension, avoid monomorphic extensions with the assumption of these
// not being performance sensitive in practice. (Maybe we have a polymorphic interface for
// the crate usage in the future.
self.extend_inner(iter.into_iter().by_ref())
}
/// Get the length as a 2-byte integer.
///
/// This is *almost* naturally bounded by the tag type being a `u16` itself. The
/// bounds are upheld when extending the iteration, i.e. this always corresponds to the
/// underlying data.
pub fn len(&self) -> u16 {
// The keys are `u16`. Since IFDs are required to have at least one entry this would have
// been a trivial thing to do in the specification by storing it minus one but alas.
self.entries.len() as u16
}
/// Get the pointer to the next IFD, if it was defined.
pub fn next(&self) -> Option<IfdPointer> {
self.next_ifd.map(|n| IfdPointer(n.get()))
}
fn extend_inner(&mut self, iter: &mut dyn Iterator<Item = (Tag, Entry)>) {
for (tag, entry) in iter {
let is_full = self.entries.len() == u16::MAX as usize;
// If the tag is already present, it will be overwritten.
let map_entry = self.entries.entry(tag.to_u16());
if is_full {
// Only allowed to modify if the entry is already present.
map_entry.and_modify(|place| *place = entry);
} else {
match map_entry {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(entry);
}
std::collections::btree_map::Entry::Occupied(mut occupied_entry) => {
occupied_entry.insert(entry);
}
}
}
}
}
}
impl fmt::Debug for Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory")
.field(
"entries",
&self.entries.iter().map(|(k, v)| (Tag::from_u16(*k), v)),
)
.field("next_ifd", &self.next_ifd)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::{Directory, IfdPointer};
use crate::{decoder::ifd::Entry, tags::Tag};
#[test]
fn directory_upholds_len_invariant() {
let mut dir = Directory::empty(IfdPointer(0));
assert_eq!(dir.len(), 0);
let bogus_entry = Entry::new_u64(crate::tags::Type::BYTE, 0x0, [0; 8]);
dir.extend((0..=u16::MAX).map(|i| {
let tag = Tag::Unknown(i);
let entry = bogus_entry.clone();
(tag, entry)
}));
assert_eq!(dir.len(), u16::MAX);
// Ensure the other tags are still writable.
dir.extend([(
Tag::Unknown(0),
Entry::new_u64(crate::tags::Type::BYTE, 0x42, [0; 8]),
)]);
assert_eq!(
dir.get(Tag::Unknown(0))
.expect("tag 0 should be present after overwriting")
.count(),
0x42
);
}
#[test]
fn directory_multiple_entries() {
let mut dir = Directory::empty(IfdPointer(0));
assert_eq!(dir.len(), 0);
dir.extend((0..=u16::MAX).map(|i| {
let tag = Tag::Unknown(1);
let entry = Entry::new_u64(crate::tags::Type::BYTE, i.into(), [0; 8]);
(tag, entry)
}));
assert_eq!(dir.len(), 1, "Only one tag was ever modified");
assert_eq!(
dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain")
.count(),
u16::MAX.into()
);
}
}
+2
View File
@@ -8,10 +8,12 @@
mod bytecast; mod bytecast;
pub mod decoder; pub mod decoder;
mod directory;
pub mod encoder; pub mod encoder;
mod error; mod error;
pub mod tags; pub mod tags;
pub use self::directory::Directory;
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError}; pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
/// An enumeration over supported color types and their bit depths /// An enumeration over supported color types and their bit depths
+20 -1
View File
@@ -78,7 +78,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
// palette-color images (PhotometricInterpretation 3) // palette-color images (PhotometricInterpretation 3)
ColorMap = 320, // TODO add support ColorMap = 320, // TODO add support
Compression = 259, // TODO add support for 2 and 32773 Compression = 259, // TODO add support for 2 and 32773
Copyright = 33_432,
DateTime = 306, DateTime = 306,
ExtraSamples = 338, // TODO add support ExtraSamples = 338, // TODO add support
FillOrder = 266, // TODO add support FillOrder = 266, // TODO add support
@@ -124,13 +123,33 @@ pub enum Tag(u16) unknown("A private or extension tag") {
ModelPixelScaleTag = 33550, // (SoftDesk) ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group) ModelTransformationTag = 34264, // (JPL Carto Group)
ModelTiepointTag = 33922, // (Intergraph) ModelTiepointTag = 33922, // (Intergraph)
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
// *Do note its typo in the Decimal id*
Copyright = 33_432,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
ExifDirectory = 0x8769,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
GpsDirectory = 0x8825,
GeoKeyDirectoryTag = 34735, // (SPOT) GeoKeyDirectoryTag = 34735, // (SPOT)
GeoDoubleParamsTag = 34736, // (SPOT) GeoDoubleParamsTag = 34736, // (SPOT)
GeoAsciiParamsTag = 34737, // (SPOT) GeoAsciiParamsTag = 34737, // (SPOT)
ExifVersion = 0x9000,
GdalNodata = 42113, // Contains areas with missing data GdalNodata = 42113, // Contains areas with missing data
} }
} }
/// Identifies the offset of an IFD.
///
/// This is represented as a 64-bit integer but only BigTIFF can utilize the bits. It is encoded
/// as 32-bit unsigned value ([`Type::LONG`]) in regular TIFF files and as 64-bit unsigned value
/// ([`Type::IFD8`]) in BigTIFF files.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
// We could be using `NonZeroU64` here but I find that this complicates semantics. This type
// represents the integer value stored as a value in a tag. The semantics of treating `0` as an end
// marker are imposed by the IFD. (It's unclear if Pointer tags such as Exif would allow `0` but in
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
tags! { tags! {
/// The type of an IFD entry (a 2 byte field). /// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) { pub enum Type(u16) {