From 98cd3dc2d296c95edd61385cfea0e9deec081b90 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:07:16 +0200 Subject: [PATCH 1/6] Refactor directory reading This allows us to recombine the reader and other file data in a Decoder with any IFD, whether that of an image or an extension IFD as indicated by an IFD pointer such as Exif and GPS. --- src/decoder/image.rs | 31 ++- src/decoder/mod.rs | 631 +++++++++++++++++++++++++----------------- src/decoder/stream.rs | 7 +- 3 files changed, 398 insertions(+), 271 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 777d5eb..cebfaf1 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -1,8 +1,8 @@ use super::ifd::Value; -use super::stream::{ByteOrder, PackBitsReader}; +use super::stream::PackBitsReader; use super::tag_reader::TagReader; -use super::{predict_f16, predict_f32, predict_f64, Limits}; -use super::{stream::EndianReader, ChunkType}; +use super::ChunkType; +use super::{predict_f16, predict_f32, predict_f64, ValueReader}; use crate::tags::{ CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag, }; @@ -82,16 +82,20 @@ pub(crate) struct Image { impl Image { pub fn from_reader( - reader: &mut EndianReader, + reader: &mut ValueReader, ifd: Directory, - limits: &Limits, - bigtiff: bool, ) -> TiffResult { + let ValueReader { + reader, + bigtiff, + limits, + } = reader; + let mut tag_reader = TagReader { reader, limits, ifd: &ifd, - bigtiff, + bigtiff: *bigtiff, }; let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?; @@ -540,13 +544,20 @@ impl Image { pub(crate) fn expand_chunk( &self, - reader: impl Read, + reader: &mut ValueReader, buf: &mut [u8], output_row_stride: usize, - byte_order: ByteOrder, chunk_index: u32, - limits: &Limits, ) -> TiffResult<()> { + let ValueReader { + reader, + bigtiff: _, + limits, + } = reader; + + let byte_order = reader.byte_order; + let reader = reader.inner(); + // Validate that the color type is supported. let color_type = self.colortype()?; match color_type { diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 66f4b9a..af09f1f 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -10,6 +10,7 @@ use crate::{ UsageError, }; use half::f16; +use ifd::Entry; use self::image::Image; use self::stream::{ByteOrder, EndianReader}; @@ -261,9 +262,9 @@ pub struct Decoder where R: Read + Seek, { - reader: EndianReader, - bigtiff: bool, - limits: Limits, + /// There are grouped for borrow checker reasons. This allows us to implement methods that + /// borrow the stream access and the other fields mutably at the same time. + value_reader: ValueReader, current_ifd: Option, next_ifd: Option, /// The IFDs we visited already in this chain of IFDs. @@ -273,6 +274,22 @@ where image: Image, } +/// All the information needed to read and interpret byte slices from the underlying file, i.e. to +/// turn an entry of a tag into an `ifd::Value` or otherwise fetch arrays of similar types. Used +/// only as the type of the field [`Decoder::value_reader`] and passed to submodules. +#[derive(Debug)] +struct ValueReader { + reader: EndianReader, + bigtiff: bool, + limits: Limits, +} + +/// Reads a directory's tag values from an underlying stream. +pub struct IfdDecoder<'lt> { + decoder: &'lt mut dyn EntryDecoder, + ifd: &'lt Directory, +} + fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { match bit_depth { 0..=8 => { @@ -503,9 +520,11 @@ impl Decoder { let ifd_offsets = vec![current_ifd]; let mut decoder = Decoder { - reader, - bigtiff, - limits: Default::default(), + value_reader: ValueReader { + reader, + bigtiff, + limits: Default::default(), + }, next_ifd, ifd_offsets, current_ifd: None, @@ -534,7 +553,7 @@ impl Decoder { } pub fn with_limits(mut self, limits: Limits) -> Decoder { - self.limits = limits; + self.value_reader.limits = limits; self } @@ -584,10 +603,10 @@ impl Decoder { // 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 = Self::read_ifd(&mut self.reader, self.bigtiff, *ifd_offset)?; + let ifd = self.value_reader.read_directory(*ifd_offset)?; self.next_ifd = ifd.next(); self.current_ifd = Some(*ifd_offset); - self.image = Image::from_reader(&mut self.reader, ifd, &self.limits, self.bigtiff)?; + self.image = Image::from_reader(&mut self.value_reader, ifd)?; Ok(()) } else { @@ -598,24 +617,23 @@ impl Decoder { } fn next_ifd(&mut self) -> TiffResult { - if self.next_ifd.is_none() { + let Some(next_ifd) = self.next_ifd.take() else { return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, )); - } + }; - let ifd_offset = self.next_ifd.take().unwrap(); - let ifd = Self::read_ifd(&mut self.reader, self.bigtiff, ifd_offset)?; + let ifd = self.value_reader.read_directory(next_ifd)?; // Ensure this walk does not get us into a cycle. - self.seen_ifds.insert_next(ifd_offset, ifd.next())?; + self.seen_ifds.insert_next(next_ifd, ifd.next())?; // Extend the list of known IFD offsets in this chain, if needed. if self.ifd_offsets.last().copied() == self.current_ifd { - self.ifd_offsets.push(ifd_offset); + self.ifd_offsets.push(next_ifd); } - self.current_ifd = Some(ifd_offset); + self.current_ifd = Some(next_ifd); self.next_ifd = ifd.next(); Ok(ifd) @@ -626,7 +644,7 @@ impl Decoder { /// To determine whether there are more images call `TIFFDecoder::more_images` instead. pub fn next_image(&mut self) -> TiffResult<()> { 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.value_reader, ifd)?; Ok(()) } @@ -637,12 +655,12 @@ impl Decoder { /// Returns the byte_order of the file. pub fn byte_order(&self) -> ByteOrder { - self.reader.byte_order + self.value_reader.reader.byte_order } #[inline] pub fn read_ifd_offset(&mut self) -> Result { - if self.bigtiff { + if self.value_reader.bigtiff { self.read_long8() } else { self.read_long().map(u64::from) @@ -653,61 +671,61 @@ impl Decoder { #[inline] pub fn read_byte(&mut self) -> Result { let mut buf = [0; 1]; - self.reader.inner().read_exact(&mut buf)?; + self.value_reader.reader.inner().read_exact(&mut buf)?; Ok(buf[0]) } /// Reads a TIFF short value #[inline] pub fn read_short(&mut self) -> Result { - self.reader.read_u16() + self.value_reader.reader.read_u16() } /// Reads a TIFF sshort value #[inline] pub fn read_sshort(&mut self) -> Result { - self.reader.read_i16() + self.value_reader.reader.read_i16() } /// Reads a TIFF long value #[inline] pub fn read_long(&mut self) -> Result { - self.reader.read_u32() + self.value_reader.reader.read_u32() } /// Reads a TIFF slong value #[inline] pub fn read_slong(&mut self) -> Result { - self.reader.read_i32() + self.value_reader.reader.read_i32() } /// Reads a TIFF float value #[inline] pub fn read_float(&mut self) -> Result { - self.reader.read_f32() + self.value_reader.reader.read_f32() } /// Reads a TIFF double value #[inline] pub fn read_double(&mut self) -> Result { - self.reader.read_f64() + self.value_reader.reader.read_f64() } #[inline] pub fn read_long8(&mut self) -> Result { - self.reader.read_u64() + self.value_reader.reader.read_u64() } #[inline] pub fn read_slong8(&mut self) -> Result { - self.reader.read_i64() + self.value_reader.reader.read_i64() } /// Reads a string #[inline] pub fn read_string(&mut self, length: usize) -> TiffResult { let mut out = vec![0; length]; - self.reader.inner().read_exact(&mut out)?; + self.value_reader.reader.inner().read_exact(&mut out)?; // Strings may be null-terminated, so we trim anything downstream of the null byte if let Some(first) = out.iter().position(|&b| b == 0) { out.truncate(first); @@ -718,13 +736,13 @@ impl Decoder { /// Reads a TIFF IFA offset/value field #[inline] pub fn read_offset(&mut self) -> TiffResult<[u8; 4]> { - if self.bigtiff { + if self.value_reader.bigtiff { return Err(TiffError::FormatError( TiffFormatError::InconsistentSizesEncountered, )); } let mut val = [0; 4]; - self.reader.inner().read_exact(&mut val)?; + self.value_reader.reader.inner().read_exact(&mut val)?; Ok(val) } @@ -732,7 +750,7 @@ impl Decoder { #[inline] pub fn read_offset_u64(&mut self) -> Result<[u8; 8], io::Error> { let mut val = [0; 8]; - self.reader.inner().read_exact(&mut val)?; + self.value_reader.reader.inner().read_exact(&mut val)?; Ok(val) } @@ -744,211 +762,7 @@ impl Decoder { #[inline] pub fn goto_offset_u64(&mut self, offset: u64) -> io::Result<()> { - 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 { - Self::read_ifd(&mut self.reader, self.bigtiff, ptr) - } - - /// Reads a IFD entry. - // An IFD entry has four fields: - // - // Tag 2 bytes - // Type 2 bytes - // Count 4 bytes - // Value 4 bytes either a pointer the value itself - fn read_entry( - reader: &mut EndianReader, - bigtiff: bool, - ) -> TiffResult> { - let tag = Tag::from_u16_exhaustive(reader.read_u16()?); - let type_ = match Type::from_u16(reader.read_u16()?) { - Some(t) => t, - None => { - // Unknown type. Skip this entry according to spec. - reader.read_u32()?; - reader.read_u32()?; - return Ok(None); - } - }; - let entry = if bigtiff { - let mut offset = [0; 8]; - - let count = reader.read_u64()?; - reader.inner().read_exact(&mut offset)?; - ifd::Entry::new_u64(type_, count, offset) - } else { - let mut offset = [0; 4]; - - let count = reader.read_u32()?; - reader.inner().read_exact(&mut offset)?; - ifd::Entry::new(type_, count, offset) - }; - Ok(Some((tag, entry))) - } - - /// Reads the IFD starting at the indicated location. - fn read_ifd( - reader: &mut EndianReader, - bigtiff: bool, - ifd_location: IfdPointer, - ) -> TiffResult { - reader.goto_offset(ifd_location.0)?; - - 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, - None => { - continue; - } // Unknown data type in tag, skip - }; - - entries.insert(tag.to_u16(), entry); - } - - let next_ifd = if bigtiff { - reader.read_u64()? - } else { - reader.read_u32()?.into() - }; - - let next_ifd = core::num::NonZeroU64::new(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> { - let entry = match self.image().ifd.as_ref().unwrap().get(tag) { - None => return Ok(None), - Some(entry) => entry.clone(), - }; - - Ok(Some(entry.val( - &self.limits, - self.bigtiff, - &mut self.reader, - )?)) - } - - /// Tries to retrieve a tag and convert it to the desired unsigned type. - pub fn find_tag_unsigned>(&mut self, tag: Tag) -> TiffResult> { - self.find_tag(tag)? - .map(|v| v.into_u64()) - .transpose()? - .map(|value| { - T::try_from(value).map_err(|_| TiffFormatError::InvalidTagValueType(tag).into()) - }) - .transpose() - } - - /// Tries to retrieve a vector of all a tag's values and convert them to - /// the desired unsigned type. - pub fn find_tag_unsigned_vec>( - &mut self, - tag: Tag, - ) -> TiffResult>> { - self.find_tag(tag)? - .map(|v| v.into_u64_vec()) - .transpose()? - .map(|v| { - v.into_iter() - .map(|u| { - T::try_from(u).map_err(|_| TiffFormatError::InvalidTagValueType(tag).into()) - }) - .collect() - }) - .transpose() - } - - /// Tries to retrieve a tag and convert it to the desired unsigned type. - /// Returns an error if the tag is not present. - pub fn get_tag_unsigned>(&mut self, tag: Tag) -> TiffResult { - self.find_tag_unsigned(tag)? - .ok_or_else(|| TiffFormatError::RequiredTagNotFound(tag).into()) - } - - /// Tries to retrieve a tag. - /// Returns an error if the tag is not present - pub fn get_tag(&mut self, tag: Tag) -> TiffResult { - match self.find_tag(tag)? { - Some(val) => Ok(val), - None => Err(TiffError::FormatError( - TiffFormatError::RequiredTagNotFound(tag), - )), - } - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_u32(&mut self, tag: Tag) -> TiffResult { - self.get_tag(tag)?.into_u32() - } - pub fn get_tag_u64(&mut self, tag: Tag) -> TiffResult { - self.get_tag(tag)?.into_u64() - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_f32(&mut self, tag: Tag) -> TiffResult { - self.get_tag(tag)?.into_f32() - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_f64(&mut self, tag: Tag) -> TiffResult { - self.get_tag(tag)?.into_f64() - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_u32_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_u32_vec() - } - - pub fn get_tag_u16_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_u16_vec() - } - pub fn get_tag_u64_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_u64_vec() - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_f32_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_f32_vec() - } - - /// Tries to retrieve a tag and convert it to the desired type. - pub fn get_tag_f64_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_f64_vec() - } - - /// Tries to retrieve a tag and convert it to a 8bit vector. - pub fn get_tag_u8_vec(&mut self, tag: Tag) -> TiffResult> { - self.get_tag(tag)?.into_u8_vec() - } - - /// Tries to retrieve a tag and convert it to a ascii vector. - pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult { - self.get_tag(tag)?.into_string() - } - - /// Returns an iterator over all tags in the current image, along with their values. - pub fn tag_iter(&mut self) -> impl Iterator> + '_ { - self.image.ifd.as_ref().unwrap().iter().map(|(tag, entry)| { - entry - .val(&self.limits, self.bigtiff, &mut self.reader) - .map(|value| (tag, value)) - }) + self.value_reader.reader.goto_offset(offset) } fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> { @@ -1003,9 +817,7 @@ impl Decoder { output_width: usize, ) -> TiffResult<()> { let offset = self.image.chunk_file_range(chunk_index)?.0; - self.reader.goto_offset(offset)?; - - let byte_order = self.reader.byte_order; + self.goto_offset_u64(offset)?; let output_row_stride = (output_width as u64) .saturating_mul(self.image.samples_per_pixel() as u64) @@ -1013,12 +825,10 @@ impl Decoder { / 8; self.image.expand_chunk( - self.reader.inner(), + &mut self.value_reader, buffer.as_bytes_mut(), output_row_stride.try_into()?, - byte_order, chunk_index, - &self.limits, )?; Ok(()) @@ -1041,29 +851,31 @@ impl Decoder { .ok_or(TiffError::LimitsExceeded)?; let max_sample_bits = self.image().bits_per_sample; + let limits = &self.value_reader.limits; + match self.image().sample_format { SampleFormat::Uint => match max_sample_bits { - n if n <= 8 => DecodingResult::new_u8(buffer_size, &self.limits), - n if n <= 16 => DecodingResult::new_u16(buffer_size, &self.limits), - n if n <= 32 => DecodingResult::new_u32(buffer_size, &self.limits), - n if n <= 64 => DecodingResult::new_u64(buffer_size, &self.limits), + n if n <= 8 => DecodingResult::new_u8(buffer_size, limits), + n if n <= 16 => DecodingResult::new_u16(buffer_size, limits), + n if n <= 32 => DecodingResult::new_u32(buffer_size, limits), + n if n <= 64 => DecodingResult::new_u64(buffer_size, limits), n => Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedBitsPerChannel(n), )), }, SampleFormat::IEEEFP => match max_sample_bits { - 16 => DecodingResult::new_f16(buffer_size, &self.limits), - 32 => DecodingResult::new_f32(buffer_size, &self.limits), - 64 => DecodingResult::new_f64(buffer_size, &self.limits), + 16 => DecodingResult::new_f16(buffer_size, limits), + 32 => DecodingResult::new_f32(buffer_size, limits), + 64 => DecodingResult::new_f64(buffer_size, limits), n => Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedBitsPerChannel(n), )), }, SampleFormat::Int => match max_sample_bits { - n if n <= 8 => DecodingResult::new_i8(buffer_size, &self.limits), - n if n <= 16 => DecodingResult::new_i16(buffer_size, &self.limits), - n if n <= 32 => DecodingResult::new_i32(buffer_size, &self.limits), - n if n <= 64 => DecodingResult::new_i64(buffer_size, &self.limits), + n if n <= 8 => DecodingResult::new_i8(buffer_size, limits), + n if n <= 16 => DecodingResult::new_i16(buffer_size, limits), + n if n <= 32 => DecodingResult::new_i32(buffer_size, limits), + n if n <= 64 => DecodingResult::new_i64(buffer_size, limits), n => Err(TiffError::UnsupportedError( TiffUnsupportedError::UnsupportedBitsPerChannel(n), )), @@ -1148,23 +960,324 @@ impl Decoder { // * pass requested band as parameter // * collect bands to a RGB encoding result in case of RGB bands for chunk in 0..image_chunks { - self.reader.goto_offset(self.image().chunk_offsets[chunk])?; + self.goto_offset_u64(self.image().chunk_offsets[chunk])?; let x = chunk % chunks_across; let y = chunk / chunks_across; let buffer_offset = y * output_row_stride * chunk_dimensions.1 as usize + x * chunk_row_bytes; - let byte_order = self.reader.byte_order; self.image.expand_chunk( - self.reader.inner(), + &mut self.value_reader, &mut result.as_buffer(0).as_bytes_mut()[buffer_offset..], output_row_stride, - byte_order, chunk as u32, - &self.limits, )?; } Ok(result) } + + /// Get the IFD decoder for our current image IFD. + fn image_ifd(&mut self) -> IfdDecoder<'_> { + IfdDecoder { + decoder: &mut self.value_reader, + ifd: self.image.ifd.as_ref().unwrap(), + } + } + + /// Tries to retrieve a tag from the current image directory. + /// Return `Ok(None)` if the tag is not present. + pub fn find_tag(&mut self, tag: Tag) -> TiffResult> { + self.image_ifd().find_tag(tag) + } + + /// Tries to retrieve a tag in the current image directory and convert it to the desired + /// unsigned type. + pub fn find_tag_unsigned>(&mut self, tag: Tag) -> TiffResult> { + self.image_ifd().find_tag_unsigned(tag) + } + + /// Tries to retrieve a tag from the current image directory and convert it to the desired + /// unsigned type. Returns an error if the tag is not present. + pub fn get_tag_unsigned>(&mut self, tag: Tag) -> TiffResult { + self.image_ifd().get_tag_unsigned(tag) + } + + /// Tries to retrieve a tag from the current image directory. + /// Returns an error if the tag is not present + pub fn get_tag(&mut self, tag: Tag) -> TiffResult { + self.image_ifd().get_tag(tag) + } + + pub fn get_tag_u32(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_u32() + } + + pub fn get_tag_u64(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_u64() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f32(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_f32() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f64(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_f64() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_u32_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u32_vec() + } + + pub fn get_tag_u16_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u16_vec() + } + + pub fn get_tag_u64_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u64_vec() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f32_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_f32_vec() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f64_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_f64_vec() + } + + /// Tries to retrieve a tag and convert it to a 8bit vector. + pub fn get_tag_u8_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u8_vec() + } + + /// Tries to retrieve a tag and convert it to a ascii vector. + pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_string() + } + + pub fn tag_iter(&mut self) -> impl Iterator> + '_ { + self.image_ifd().tag_iter() + } +} + +impl ValueReader { + pub(crate) fn read_directory(&mut self, ptr: IfdPointer) -> Result { + Self::read_ifd(&mut self.reader, self.bigtiff, ptr) + } + + /// Reads a IFD entry. + // An IFD entry has four fields: + // + // Tag 2 bytes + // Type 2 bytes + // Count 4 bytes + // Value 4 bytes either a pointer the value itself + fn read_entry( + reader: &mut EndianReader, + bigtiff: bool, + ) -> TiffResult> { + let tag = Tag::from_u16_exhaustive(reader.read_u16()?); + let type_ = match Type::from_u16(reader.read_u16()?) { + Some(t) => t, + None => { + // Unknown type. Skip this entry according to spec. + reader.read_u32()?; + reader.read_u32()?; + return Ok(None); + } + }; + let entry = if bigtiff { + let mut offset = [0; 8]; + + let count = reader.read_u64()?; + reader.inner().read_exact(&mut offset)?; + ifd::Entry::new_u64(type_, count, offset) + } else { + let mut offset = [0; 4]; + + let count = reader.read_u32()?; + reader.inner().read_exact(&mut offset)?; + ifd::Entry::new(type_, count, offset) + }; + Ok(Some((tag, entry))) + } + + /// Reads the IFD starting at the indicated location. + fn read_ifd( + reader: &mut EndianReader, + bigtiff: bool, + ifd_location: IfdPointer, + ) -> TiffResult { + reader.goto_offset(ifd_location.0)?; + + 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, + None => { + continue; + } // Unknown data type in tag, skip + }; + + entries.insert(tag.to_u16(), entry); + } + + let next_ifd = if bigtiff { + reader.read_u64()? + } else { + reader.read_u32()?.into() + }; + + let next_ifd = core::num::NonZeroU64::new(next_ifd); + + Ok(Directory { entries, next_ifd }) + } +} + +trait EntryDecoder { + /// Turn an entry into a value by fetching the required bytes in the stream. + fn entry_val(&mut self, entry: &Entry) -> TiffResult; +} + +impl EntryDecoder for ValueReader { + fn entry_val(&mut self, entry: &Entry) -> TiffResult { + entry.val(&self.limits, self.bigtiff, &mut self.reader) + } +} + +impl IfdDecoder<'_> { + /// Tries to retrieve a tag. + /// Return `Ok(None)` if the tag is not present. + pub fn find_tag(&mut self, tag: Tag) -> TiffResult> { + let entry = match self.ifd.get(tag) { + None => return Ok(None), + Some(entry) => entry.clone(), + }; + + self.decoder.entry_val(&entry).map(Some) + } + + /// Tries to retrieve a tag and convert it to the desired unsigned type. + pub fn find_tag_unsigned>(&mut self, tag: Tag) -> TiffResult> { + self.find_tag(tag)? + .map(|v| v.into_u64()) + .transpose()? + .map(|value| { + T::try_from(value).map_err(|_| TiffFormatError::InvalidTagValueType(tag).into()) + }) + .transpose() + } + + /// Tries to retrieve a vector of all a tag's values and convert them to + /// the desired unsigned type. + pub fn find_tag_unsigned_vec>( + &mut self, + tag: Tag, + ) -> TiffResult>> { + self.find_tag(tag)? + .map(|v| v.into_u64_vec()) + .transpose()? + .map(|v| { + v.into_iter() + .map(|u| { + T::try_from(u).map_err(|_| TiffFormatError::InvalidTagValueType(tag).into()) + }) + .collect() + }) + .transpose() + } + + /// Tries to retrieve a tag and convert it to the desired unsigned type. + /// Returns an error if the tag is not present. + pub fn get_tag_unsigned>(&mut self, tag: Tag) -> TiffResult { + self.find_tag_unsigned(tag)? + .ok_or_else(|| TiffFormatError::RequiredTagNotFound(tag).into()) + } + + /// Tries to retrieve a tag. + /// Returns an error if the tag is not present + pub fn get_tag(&mut self, tag: Tag) -> TiffResult { + match self.find_tag(tag)? { + Some(val) => Ok(val), + None => Err(TiffError::FormatError( + TiffFormatError::RequiredTagNotFound(tag), + )), + } + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_u32(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_u32() + } + + pub fn get_tag_u64(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_u64() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f32(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_f32() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f64(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_f64() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_u32_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u32_vec() + } + + pub fn get_tag_u16_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u16_vec() + } + + pub fn get_tag_u64_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u64_vec() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f32_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_f32_vec() + } + + /// Tries to retrieve a tag and convert it to the desired type. + pub fn get_tag_f64_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_f64_vec() + } + + /// Tries to retrieve a tag and convert it to a 8bit vector. + pub fn get_tag_u8_vec(&mut self, tag: Tag) -> TiffResult> { + self.get_tag(tag)?.into_u8_vec() + } + + /// Tries to retrieve a tag and convert it to a ascii vector. + pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult { + self.get_tag(tag)?.into_string() + } +} + +impl<'l> IfdDecoder<'l> { + /// Returns an iterator over all tags in the current image, along with their values. + pub fn tag_iter(self) -> impl Iterator> + 'l { + self.ifd + .iter() + .map(|(tag, entry)| match self.decoder.entry_val(entry) { + Ok(value) => Ok((tag, value)), + Err(err) => Err(err), + }) + } } diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 979f080..242128f 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -18,7 +18,7 @@ pub struct EndianReader { pub(crate) byte_order: ByteOrder, } -impl EndianReader { +impl EndianReader { pub fn new(reader: R, byte_order: ByteOrder) -> Self { Self { reader, byte_order } } @@ -27,7 +27,10 @@ impl EndianReader { &mut self.reader } - pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> { + pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> + where + R: Seek, + { self.reader.seek(io::SeekFrom::Start(offset))?; Ok(()) } From f33c7be21cd5f81996039980117b8cf333f136f9 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:10:39 +0200 Subject: [PATCH 2/6] Add the SubIfd tag --- src/tags.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tags.rs b/src/tags.rs index 05ca1f7..9a9f9f3 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -113,6 +113,7 @@ pub enum Tag(u16) unknown("A private or extension tag") { TileLength = 323, TileOffsets = 324, TileByteCounts = 325, + SubIfd = 330, // Data Sample Format SampleFormat = 339, SMinSampleValue = 340, // TODO add support From f5ab3b363b33a8f4f6978473514b164210b7bdaa Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:10:52 +0200 Subject: [PATCH 3/6] Expose interacting with secondary IFDs --- src/decoder/ifd.rs | 32 +++++++++++++++++++++++++++++++- src/decoder/mod.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/decoder/ifd.rs b/src/decoder/ifd.rs index 455a3e8..8b9aa8f 100644 --- a/src/decoder/ifd.rs +++ b/src/decoder/ifd.rs @@ -6,7 +6,7 @@ use std::mem; use std::str; use super::stream::{ByteOrder, EndianReader}; -use crate::tags::{Tag, Type}; +use crate::tags::{IfdPointer, Tag, Type}; use crate::{TiffError, TiffFormatError, TiffResult}; use self::Value::{ @@ -130,6 +130,25 @@ impl Value { } } + /// Turn this value into an `IfdPointer`. + /// + /// Notice that this does not take an argument, a 64-bit IFD is always allowed. If the + /// difference is crucial and you do not want to be permissive you're expected to filter this + /// out before. + /// + /// For compatibility the smaller sized tags should always be allowed i.e. you might use a + /// non-bigtiff's directory and its tag types and move it straight to a bigtiff. For instance + /// the SubIFD tag is defined as `LONG or IFD`: + /// + /// + pub fn into_ifd_pointer(self) -> TiffResult { + match self { + Unsigned(val) | Ifd(val) => Ok(IfdPointer(val.into())), + IfdBig(val) => Ok(IfdPointer(val)), + _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), + } + } + pub fn into_string(self) -> TiffResult { match self { Ascii(val) => Ok(val), @@ -300,6 +319,17 @@ impl Value { _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), } } + + pub fn into_ifd_vec(self) -> TiffResult> { + let vec = match self { + Unsigned(val) | Ifd(val) => return Ok(vec![IfdPointer(val.into())]), + IfdBig(val) => return Ok(vec![IfdPointer(val)]), + List(vec) => vec, + _ => return Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), + }; + + vec.into_iter().map(Self::into_ifd_pointer).collect() + } } #[derive(Clone)] diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index af09f1f..3ed7d2d 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -765,6 +765,13 @@ impl Decoder { self.value_reader.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 { + self.value_reader.read_directory(ptr) + } + fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> { if expected != self.image().chunk_type { return Err(TiffError::UsageError(UsageError::InvalidChunkType( @@ -985,6 +992,37 @@ impl Decoder { } } + /// Prepare reading tag values for some externally given directory. + /// + /// # Examples + /// + /// This method may be used to read the values of tags in directories that have been previously + /// read with [`Decoder::read_directory`]. + /// + /// ```no_run + /// use tiff::decoder::Decoder; + /// use tiff::tags::Tag; + /// + /// # use std::io::Cursor; + /// # let mut data = Cursor::new(vec![0]); + /// let mut decoder = Decoder::new(&mut data).unwrap(); + /// let sub_ifds = decoder.get_tag(Tag::SubIfd)?.into_ifd_vec()?; + /// + /// for ifd in sub_ifds { + /// let subdir = decoder.read_directory(ifd)?; + /// let subfile = decoder.with_ifd(&subdir).find_tag(Tag::SubfileType)?; + /// // todo: handle the subfiles, e.g. thumbnails + /// } + /// + /// # Ok::<_, tiff::TiffError>(()) + /// ``` + pub fn with_ifd<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { + IfdDecoder { + decoder: &mut self.value_reader, + ifd, + } + } + /// Tries to retrieve a tag from the current image directory. /// Return `Ok(None)` if the tag is not present. pub fn find_tag(&mut self, tag: Tag) -> TiffResult> { From f021e3e5990a8966a12a4a847fb1751243870acd Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:49:22 +0200 Subject: [PATCH 4/6] Rename ifd value adapter to read_directory_tags --- src/decoder/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 3ed7d2d..1e8e535 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1010,13 +1010,13 @@ impl Decoder { /// /// for ifd in sub_ifds { /// let subdir = decoder.read_directory(ifd)?; - /// let subfile = decoder.with_ifd(&subdir).find_tag(Tag::SubfileType)?; - /// // todo: handle the subfiles, e.g. thumbnails + /// let subfile = decoder.read_directory_tags(&subdir).find_tag(Tag::SubfileType)?; + /// // omitted: handle the subfiles, e.g. thumbnails /// } /// /// # Ok::<_, tiff::TiffError>(()) /// ``` - pub fn with_ifd<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { + pub fn read_directory_tags<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { IfdDecoder { decoder: &mut self.value_reader, ifd, From e62cdfbf421b492f0c203b28415d90a88e5ba325 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:03:40 +0200 Subject: [PATCH 5/6] Merge TagReader and exposed IfdReader internals Having the value reader itself take care of the bigtiff and limit semantics is a little cleaner, with the TagReader being responsible for the interface of grabbing specific types of IFD values. --- src/decoder/image.rs | 15 ++------------ src/decoder/mod.rs | 39 +++++++++++++------------------------ src/decoder/tag_reader.rs | 41 ++++++++++++++++++++++++++------------- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index cebfaf1..49510a3 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -82,21 +82,10 @@ pub(crate) struct Image { impl Image { pub fn from_reader( - reader: &mut ValueReader, + decoder: &mut ValueReader, ifd: Directory, ) -> TiffResult { - let ValueReader { - reader, - bigtiff, - limits, - } = reader; - - let mut tag_reader = TagReader { - reader, - limits, - ifd: &ifd, - bigtiff: *bigtiff, - }; + let mut tag_reader = TagReader { decoder, ifd: &ifd }; let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?; let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 1e8e535..ed2fdd2 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -10,7 +10,6 @@ use crate::{ UsageError, }; use half::f16; -use ifd::Entry; use self::image::Image; use self::stream::{ByteOrder, EndianReader}; @@ -286,8 +285,7 @@ struct ValueReader { /// Reads a directory's tag values from an underlying stream. pub struct IfdDecoder<'lt> { - decoder: &'lt mut dyn EntryDecoder, - ifd: &'lt Directory, + inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>, } fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { @@ -987,8 +985,10 @@ impl Decoder { /// Get the IFD decoder for our current image IFD. fn image_ifd(&mut self) -> IfdDecoder<'_> { IfdDecoder { - decoder: &mut self.value_reader, - ifd: self.image.ifd.as_ref().unwrap(), + inner: tag_reader::TagReader { + decoder: &mut self.value_reader, + ifd: self.image.ifd.as_ref().unwrap(), + }, } } @@ -1018,8 +1018,10 @@ impl Decoder { /// ``` pub fn read_directory_tags<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { IfdDecoder { - decoder: &mut self.value_reader, - ifd, + inner: tag_reader::TagReader { + decoder: &mut self.value_reader, + ifd, + }, } } @@ -1184,27 +1186,11 @@ impl ValueReader { } } -trait EntryDecoder { - /// Turn an entry into a value by fetching the required bytes in the stream. - fn entry_val(&mut self, entry: &Entry) -> TiffResult; -} - -impl EntryDecoder for ValueReader { - fn entry_val(&mut self, entry: &Entry) -> TiffResult { - entry.val(&self.limits, self.bigtiff, &mut self.reader) - } -} - impl IfdDecoder<'_> { /// Tries to retrieve a tag. /// Return `Ok(None)` if the tag is not present. pub fn find_tag(&mut self, tag: Tag) -> TiffResult> { - let entry = match self.ifd.get(tag) { - None => return Ok(None), - Some(entry) => entry.clone(), - }; - - self.decoder.entry_val(&entry).map(Some) + self.inner.find_tag(tag) } /// Tries to retrieve a tag and convert it to the desired unsigned type. @@ -1311,9 +1297,10 @@ impl IfdDecoder<'_> { impl<'l> IfdDecoder<'l> { /// Returns an iterator over all tags in the current image, along with their values. pub fn tag_iter(self) -> impl Iterator> + 'l { - self.ifd + self.inner + .ifd .iter() - .map(|(tag, entry)| match self.decoder.entry_val(entry) { + .map(|(tag, entry)| match self.inner.decoder.entry_val(entry) { Ok(value) => Ok((tag, value)), Err(err) => Err(err), }) diff --git a/src/decoder/tag_reader.rs b/src/decoder/tag_reader.rs index c6d340f..99bba57 100644 --- a/src/decoder/tag_reader.rs +++ b/src/decoder/tag_reader.rs @@ -3,23 +3,35 @@ use std::io::{Read, Seek}; use crate::{tags::Tag, Directory}; use crate::{TiffError, TiffFormatError, TiffResult}; -use super::ifd::Value; -use super::stream::EndianReader; -use super::Limits; +use super::ifd::{Entry, Value}; +use super::ValueReader; -pub(crate) struct TagReader<'a, R: Read + Seek> { - pub reader: &'a mut EndianReader, - pub ifd: &'a Directory, - pub limits: &'a Limits, - pub bigtiff: bool, +pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> { + pub(crate) decoder: &'lt mut R, + pub(crate) ifd: &'lt Directory, } -impl<'a, R: Read + Seek> TagReader<'a, R> { - pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult> { - Ok(match self.ifd.get(tag) { - Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?), - None => None, - }) + +pub(crate) trait EntryDecoder { + /// Turn an entry into a value by fetching the required bytes in the stream. + fn entry_val(&mut self, entry: &Entry) -> TiffResult; +} + +impl EntryDecoder for ValueReader { + fn entry_val(&mut self, entry: &Entry) -> TiffResult { + entry.val(&self.limits, self.bigtiff, &mut self.reader) } +} + +impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> { + pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult> { + let entry = match self.ifd.get(tag) { + None => return Ok(None), + Some(entry) => entry.clone(), + }; + + self.decoder.entry_val(&entry).map(Some) + } + pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult { match self.find_tag(tag)? { Some(val) => Ok(val), @@ -28,6 +40,7 @@ impl<'a, R: Read + Seek> TagReader<'a, R> { )), } } + pub fn find_tag_uint_vec>(&mut self, tag: Tag) -> TiffResult>> { self.find_tag(tag)? .map(|v| v.into_u64_vec()) From 26b6afc787797a2246b206e444077612496eb589 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:34:46 +0200 Subject: [PATCH 6/6] Document Directory related symbols --- src/decoder/mod.rs | 22 +++++++++++++++++++--- src/directory.rs | 10 ++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index ed2fdd2..0f91d23 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -763,9 +763,25 @@ impl Decoder { self.value_reader.reader.goto_offset(offset) } - /// Read a directory from a known offset. + /// Read a tag-entry map from a known offset. /// - /// This may always modify the position of the reader. + /// A TIFF [`Directory`], aka. image file directory aka. IFD, refers to a map from + /// tags–identified by a `u16`–to a typed vector of elements. It is encoded as a list + /// of ascending tag values with the offset and type of their corresponding values. The + /// semantic interpretations of a tag and its type requirements depend on the context of the + /// directory. The main image directories, those iterated over by the `Decoder` after + /// construction, are represented by [`Tag`] and [`ifd::Value`]. Other forms are EXIF and GPS + /// data as well as thumbnail Sub-IFD representations associated with each image file. + /// + /// This method allows the decoding of a directory from an arbitrary offset in the image file + /// with no specific semantic interpretation. Such an offset is usually found as the value of + /// a tag, e.g. [`Tag::SubIfd`], [`Tag::ExifDirectory`], [`Tag::GpsDirectory`] and recovered + /// from the associated value by [`ifd::Value::into_ifd_pointer`]. + /// + /// The library will not verify whether the offset overlaps any other directory or would form a + /// cycle with any other directory when calling this method. This will modify the position of + /// the reader, i.e. continuing with direct reads at a later point will require going back with + /// [`Self::goto_offset`]. pub fn read_directory(&mut self, ptr: IfdPointer) -> TiffResult { self.value_reader.read_directory(ptr) } @@ -992,7 +1008,7 @@ impl Decoder { } } - /// Prepare reading tag values for some externally given directory. + /// Prepare reading values for tags of a given directory. /// /// # Examples /// diff --git a/src/directory.rs b/src/directory.rs index 5cb2c52..40c74dc 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -7,6 +7,16 @@ use crate::{ }; /// An Image File Directory (IFD). +/// +/// A directory is a map of [`Tag`]s to [`Value`](crate::decoder::ifd::Value)s. The values are +/// stored anywhere in the file, with the directory containing the offsets and length of the +/// associated values for each tag present in the directory. +/// +/// A directory can be created with with +/// [`Decoder::read_directory`](crate::decoder::Decoder::read_directory) or as an empty directory +/// to be extended with entries. A directory may be used with +/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the +/// values associated with tags from an underlying file. #[doc(alias = "IFD")] pub struct Directory { /// There are at most `u16::MAX` entries in any single directory, the count is stored as a