Add a dedicated Directory type (#277)
This commit is contained in:
1 parent
816fb85304
commit
df4e42eecc
7 files changed
+239
-43
No files matched your search
@@ -333,6 +333,10 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u64 {
|
||||
self.count
|
||||
}
|
||||
|
||||
/// Returns a mem_reader for the offset/value field
|
||||
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
|
||||
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
|
||||
#[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>;
|
||||
+10
-8
@@ -1,4 +1,4 @@
|
||||
use super::ifd::{Directory, Value};
|
||||
use super::ifd::Value;
|
||||
use super::stream::{ByteOrder, PackBitsReader};
|
||||
use super::tag_reader::TagReader;
|
||||
use super::{predict_f16, predict_f32, predict_f64, Limits};
|
||||
@@ -6,7 +6,9 @@ use super::{stream::EndianReader, ChunkType};
|
||||
use crate::tags::{
|
||||
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::sync::Arc;
|
||||
|
||||
@@ -115,7 +117,7 @@ impl Image {
|
||||
};
|
||||
|
||||
let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG
|
||||
&& ifd.contains_key(&Tag::JPEGTables)
|
||||
&& ifd.contains(Tag::JPEGTables)
|
||||
{
|
||||
let vec = tag_reader
|
||||
.find_tag(Tag::JPEGTables)?
|
||||
@@ -210,10 +212,10 @@ impl Image {
|
||||
let strip_decoder;
|
||||
let tile_attributes;
|
||||
match (
|
||||
ifd.contains_key(&Tag::StripByteCounts),
|
||||
ifd.contains_key(&Tag::StripOffsets),
|
||||
ifd.contains_key(&Tag::TileByteCounts),
|
||||
ifd.contains_key(&Tag::TileOffsets),
|
||||
ifd.contains(Tag::StripByteCounts),
|
||||
ifd.contains(Tag::StripOffsets),
|
||||
ifd.contains(Tag::TileByteCounts),
|
||||
ifd.contains(Tag::TileOffsets),
|
||||
) {
|
||||
(true, true, false, false) => {
|
||||
chunk_type = ChunkType::Strip;
|
||||
@@ -676,7 +678,7 @@ impl Image {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i, row) in buf
|
||||
for (_i, row) in buf
|
||||
.chunks_mut(output_row_stride)
|
||||
.take(data_dims.1 as usize)
|
||||
.enumerate()
|
||||
|
||||
+37
-31
@@ -1,16 +1,16 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::io::{self, Read, Seek};
|
||||
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat,
|
||||
Tag, Type,
|
||||
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||
SampleFormat, Tag, Type,
|
||||
};
|
||||
use crate::{
|
||||
bytecast, ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
|
||||
UsageError,
|
||||
};
|
||||
use half::f16;
|
||||
|
||||
use self::ifd::Directory;
|
||||
use self::image::Image;
|
||||
use self::stream::{ByteOrder, EndianReader};
|
||||
|
||||
@@ -263,9 +263,9 @@ where
|
||||
reader: EndianReader<R>,
|
||||
bigtiff: bool,
|
||||
limits: Limits,
|
||||
next_ifd: Option<u64>,
|
||||
ifd_offsets: Vec<u64>,
|
||||
seen_ifds: HashSet<u64>,
|
||||
next_ifd: Option<IfdPointer>,
|
||||
ifd_offsets: Vec<IfdPointer>,
|
||||
seen_ifds: HashSet<IfdPointer>,
|
||||
image: Image,
|
||||
}
|
||||
|
||||
@@ -487,11 +487,13 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let next_ifd = if bigtiff {
|
||||
Some(reader.read_u64()?)
|
||||
} else {
|
||||
Some(u64::from(reader.read_u32()?))
|
||||
};
|
||||
}
|
||||
.map(IfdPointer);
|
||||
|
||||
let mut seen_ifds = HashSet::new();
|
||||
seen_ifds.insert(*next_ifd.as_ref().unwrap());
|
||||
@@ -557,9 +559,9 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
|
||||
loop {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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 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)?;
|
||||
|
||||
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() {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::ImageFileDirectoryNotFound,
|
||||
));
|
||||
}
|
||||
|
||||
let (ifd, next_ifd) = Self::read_ifd(
|
||||
let ifd = Self::read_ifd(
|
||||
&mut self.reader,
|
||||
self.bigtiff,
|
||||
self.next_ifd.take().unwrap(),
|
||||
)?;
|
||||
|
||||
if let Some(next) = next_ifd {
|
||||
if let Some(next) = ifd.next() {
|
||||
if !self.seen_ifds.insert(next) {
|
||||
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
|
||||
}
|
||||
@@ -604,15 +605,14 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
self.ifd_offsets.push(next);
|
||||
}
|
||||
|
||||
Ok((ifd, next_ifd))
|
||||
Ok(ifd)
|
||||
}
|
||||
|
||||
/// Reads in the next image.
|
||||
/// 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.
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -734,6 +734,13 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
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.
|
||||
// An IFD entry has four fields:
|
||||
//
|
||||
@@ -775,17 +782,18 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
fn read_ifd(
|
||||
reader: &mut EndianReader<R>,
|
||||
bigtiff: bool,
|
||||
ifd_location: u64,
|
||||
) -> TiffResult<(Directory, Option<u64>)> {
|
||||
reader.goto_offset(ifd_location)?;
|
||||
ifd_location: IfdPointer,
|
||||
) -> TiffResult<Directory> {
|
||||
reader.goto_offset(ifd_location.0)?;
|
||||
|
||||
let mut dir: Directory = HashMap::new();
|
||||
let mut entries: BTreeMap<_, _> = BTreeMap::new();
|
||||
|
||||
let num_tags = if bigtiff {
|
||||
reader.read_u64()?
|
||||
} else {
|
||||
reader.read_u16()?.into()
|
||||
};
|
||||
|
||||
for _ in 0..num_tags {
|
||||
let (tag, entry) = match Self::read_entry(reader, bigtiff)? {
|
||||
Some(val) => val,
|
||||
@@ -793,7 +801,8 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
continue;
|
||||
} // Unknown data type in tag, skip
|
||||
};
|
||||
dir.insert(tag, entry);
|
||||
|
||||
entries.insert(tag.to_u16(), entry);
|
||||
}
|
||||
|
||||
let next_ifd = if bigtiff {
|
||||
@@ -802,18 +811,15 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
reader.read_u32()?.into()
|
||||
};
|
||||
|
||||
let next_ifd = match next_ifd {
|
||||
0 => None,
|
||||
_ => Some(next_ifd),
|
||||
};
|
||||
let next_ifd = core::num::NonZeroU64::new(next_ifd);
|
||||
|
||||
Ok((dir, next_ifd))
|
||||
Ok(Directory { entries, next_ifd })
|
||||
}
|
||||
|
||||
/// Tries to retrieve a tag.
|
||||
/// Return `Ok(None)` if the tag is not present.
|
||||
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),
|
||||
Some(entry) => entry.clone(),
|
||||
};
|
||||
@@ -928,7 +934,7 @@ impl<R: Read + Seek> Decoder<R> {
|
||||
self.image.ifd.as_ref().unwrap().iter().map(|(tag, entry)| {
|
||||
entry
|
||||
.val(&self.limits, self.bigtiff, &mut self.reader)
|
||||
.map(|value| (*tag, value))
|
||||
.map(|value| (tag, value))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::tags::Tag;
|
||||
use crate::{tags::Tag, Directory};
|
||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||
|
||||
use super::ifd::{Directory, Value};
|
||||
use super::ifd::Value;
|
||||
use super::stream::EndianReader;
|
||||
use super::Limits;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub(crate) struct TagReader<'a, R: Read + Seek> {
|
||||
}
|
||||
impl<'a, R: Read + Seek> TagReader<'a, R> {
|
||||
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)?),
|
||||
None => None,
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
mod bytecast;
|
||||
pub mod decoder;
|
||||
mod directory;
|
||||
pub mod encoder;
|
||||
mod error;
|
||||
pub mod tags;
|
||||
|
||||
pub use self::directory::Directory;
|
||||
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
|
||||
|
||||
/// An enumeration over supported color types and their bit depths
|
||||
|
||||
+20
-1
@@ -78,7 +78,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
// palette-color images (PhotometricInterpretation 3)
|
||||
ColorMap = 320, // TODO add support
|
||||
Compression = 259, // TODO add support for 2 and 32773
|
||||
Copyright = 33_432,
|
||||
DateTime = 306,
|
||||
ExtraSamples = 338, // 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)
|
||||
ModelTransformationTag = 34264, // (JPL Carto Group)
|
||||
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)
|
||||
GeoDoubleParamsTag = 34736, // (SPOT)
|
||||
GeoAsciiParamsTag = 34737, // (SPOT)
|
||||
ExifVersion = 0x9000,
|
||||
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! {
|
||||
/// The type of an IFD entry (a 2 byte field).
|
||||
pub enum Type(u16) {
|
||||
|
||||
Reference in new issue
Block a user