From 14ebf376f352ad1b77b352bff6b46db2dfc74c90 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:37:42 +0100 Subject: [PATCH 1/7] Move Directory from image into decoder proper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This prepares allowing to iterate over a TIFF file with invalid images while `Image` maintains invariants. For instance we precalculate the amount of buffer space and verify that the count of strips makes sense in the constructor—so we do not have to redo that for every other entry point. --- src/decoder/image.rs | 6 ++---- src/decoder/mod.rs | 40 +++++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 2a4366f..b0512d6 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -64,7 +64,6 @@ impl TileAttributes { #[derive(Debug)] pub(crate) struct Image { - pub ifd: Option, pub width: u32, pub height: u32, pub bits_per_sample: u8, @@ -149,9 +148,9 @@ pub(crate) struct ReadoutLayout { impl Image { pub fn from_reader( decoder: &mut ValueReader, - ifd: Directory, + ifd: &Directory, ) -> TiffResult { - let mut tag_reader = TagReader { decoder, ifd: &ifd }; + let mut tag_reader = TagReader { decoder, ifd }; let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?; let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?; @@ -435,7 +434,6 @@ impl Image { } Ok(Image { - ifd: Some(ifd), width, height, bits_per_sample, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index b2885be..0800d73 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -516,8 +516,9 @@ where /// 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, + current_ifd_pointer: Option, + next_ifd_pointer: Option, + directory: Directory, /// The IFDs we visited already in this chain of IFDs. ifd_offsets: Vec, /// Map from the ifd into the `ifd_offsets` ordered list. @@ -821,12 +822,12 @@ impl Decoder { bigtiff, limits: Default::default(), }, - next_ifd, + next_ifd_pointer: next_ifd, ifd_offsets, - current_ifd: None, + current_ifd_pointer: None, + directory: Directory::empty(), seen_ifds: cycles::IfdCycles::new(), image: Image { - ifd: None, width: 0, height: 0, bits_per_sample: 1, @@ -867,7 +868,7 @@ impl Decoder { /// The offset of the directory representing the current image. pub fn ifd_pointer(&mut self) -> Option { - self.current_ifd + self.current_ifd_pointer } fn image(&self) -> &Image { @@ -879,8 +880,8 @@ impl Decoder { // Check whether we have seen this IFD before, if so then the index will be less than the length of the list of ifd offsets if ifd_index >= self.ifd_offsets.len() { // We possibly need to load in the next IFD - if self.next_ifd.is_none() { - self.current_ifd = None; + if self.next_ifd_pointer.is_none() { + self.current_ifd_pointer = None; return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, @@ -903,10 +904,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.value_reader.read_directory(*ifd_offset)?; - self.next_ifd = ifd.next(); - self.current_ifd = Some(*ifd_offset); - self.image = Image::from_reader(&mut self.value_reader, ifd)?; + self.directory = self.value_reader.read_directory(*ifd_offset)?; + self.next_ifd_pointer = self.directory.next(); + self.current_ifd_pointer = Some(*ifd_offset); + self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; Ok(()) } else { @@ -917,7 +918,7 @@ impl Decoder { } fn next_ifd(&mut self) -> TiffResult { - let Some(next_ifd) = self.next_ifd.take() else { + let Some(next_ifd) = self.next_ifd_pointer.take() else { return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, )); @@ -929,12 +930,12 @@ impl Decoder { 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 { + if self.ifd_offsets.last().copied() == self.current_ifd_pointer { self.ifd_offsets.push(next_ifd); } - self.current_ifd = Some(next_ifd); - self.next_ifd = ifd.next(); + self.current_ifd_pointer = Some(next_ifd); + self.next_ifd_pointer = ifd.next(); Ok(ifd) } @@ -944,13 +945,14 @@ 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.value_reader, ifd)?; + self.directory = ifd; + self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; Ok(()) } /// Returns `true` if there is at least one more image available. pub fn more_images(&self) -> bool { - self.next_ifd.is_some() + self.next_ifd_pointer.is_some() } /// Returns the byte_order of the file. @@ -1527,7 +1529,7 @@ impl Decoder { IfdDecoder { inner: tag_reader::TagReader { decoder: &mut self.value_reader, - ifd: self.image.ifd.as_ref().unwrap(), + ifd: &self.directory, }, } } From 4ff88e658d7e2d193ec7c49241f84665589e747c Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 4 Mar 2026 04:38:48 +0100 Subject: [PATCH 2/7] Rewrite Decoder::seek_to_image --- src/decoder/mod.rs | 53 ++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 0800d73..4c6cfd8 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -877,47 +877,40 @@ impl Decoder { /// Loads the IFD at the specified index in the list, if one exists pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> { - // Check whether we have seen this IFD before, if so then the index will be less than the length of the list of ifd offsets - if ifd_index >= self.ifd_offsets.len() { - // We possibly need to load in the next IFD - if self.next_ifd_pointer.is_none() { - self.current_ifd_pointer = None; - - return Err(TiffError::FormatError( - TiffFormatError::ImageFileDirectoryNotFound, - )); - } - + // Check whether we have seen this IFD before, if so then the index will be less than the + // length of the list of ifd offsets If the index is within the list of ifds then we can + // load the selected image/IFD. Otherwise we iterate the chain until the selected index. + if let Some(&ifd_offset) = self.ifd_offsets.get(ifd_index) { + self.directory = self.value_reader.read_directory(ifd_offset)?; + self.current_ifd_pointer = Some(ifd_offset); + self.next_ifd_pointer = self.directory.next(); + } else { + // We do not know if we are at the last IFD in the chain if we previous did seek back. + // The `read_next_ifd_pointer` correctly handles this. loop { // Follow the list until we find the one we want, or we reach the end, whichever happens first - let ifd = self.next_ifd()?; + self.read_next_ifd_pointer()?; - if ifd.next().is_none() { + if self.directory.next().is_none() { break; } if ifd_index < self.ifd_offsets.len() { + // We only end up here if initially `ifd_index >= self.ifd_offsets.len()` and + // each loop iteration increases the length of `self.ifd_offsets` by (at most) + // one. + debug_assert!(ifd_index + 1 == self.ifd_offsets.len()); break; } } } - // 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) { - self.directory = self.value_reader.read_directory(*ifd_offset)?; - self.next_ifd_pointer = self.directory.next(); - self.current_ifd_pointer = Some(*ifd_offset); - self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; - - Ok(()) - } else { - Err(TiffError::FormatError( - TiffFormatError::ImageFileDirectoryNotFound, - )) - } + // Reaching this point implies `self.directory` represents the selected ifd. + self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; + Ok(()) } - fn next_ifd(&mut self) -> TiffResult { + fn read_next_ifd_pointer(&mut self) -> TiffResult<()> { let Some(next_ifd) = self.next_ifd_pointer.take() else { return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, @@ -936,16 +929,16 @@ impl Decoder { self.current_ifd_pointer = Some(next_ifd); self.next_ifd_pointer = ifd.next(); + self.directory = ifd; - Ok(ifd) + Ok(()) } /// 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 = self.next_ifd()?; - self.directory = ifd; + self.read_next_ifd_pointer()?; self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; Ok(()) } From cf8f96568e747f552010d2b0ebe0aee719ea3797 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:59:20 +0100 Subject: [PATCH 3/7] Expose Decoder::seek_to_directory --- src/decoder/mod.rs | 134 ++++++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 4c6cfd8..3968482 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -827,43 +827,47 @@ impl Decoder { current_ifd_pointer: None, directory: Directory::empty(), seen_ifds: cycles::IfdCycles::new(), - image: Image { - width: 0, - height: 0, - bits_per_sample: 1, - samples: 1, - extra_samples: vec![], - photometric_samples: 1, - sample_format: SampleFormat::Uint, - photometric_interpretation: PhotometricInterpretation::BlackIsZero, - compression_method: CompressionMethod::None, - jpeg_tables: None, - predictor: Predictor::None, - chunk_type: ChunkType::Strip, - planar_config: PlanarConfiguration::Chunky, - strip_decoder: None, - tile_attributes: None, - chunk_offsets: Vec::new(), - chunk_bytes: Vec::new(), - chroma_subsampling: (2, 2), - decompression_to_host_endian: false, - }, + image: Self::no_image(), }; decoder.next_image()?; Ok(decoder) } + fn no_image() -> Image { + Image { + width: 0, + height: 0, + bits_per_sample: 1, + samples: 1, + extra_samples: vec![], + photometric_samples: 1, + sample_format: SampleFormat::Uint, + photometric_interpretation: PhotometricInterpretation::BlackIsZero, + compression_method: CompressionMethod::None, + jpeg_tables: None, + predictor: Predictor::None, + chunk_type: ChunkType::Strip, + planar_config: PlanarConfiguration::Chunky, + strip_decoder: None, + tile_attributes: None, + chunk_offsets: Vec::new(), + chunk_bytes: Vec::new(), + chroma_subsampling: (2, 2), + decompression_to_host_endian: false, + } + } + pub fn with_limits(mut self, limits: Limits) -> Decoder { self.value_reader.limits = limits; self } pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> { - Ok((self.image().width, self.image().height)) + Ok((self.image.width, self.image.height)) } pub fn colortype(&mut self) -> TiffResult { - self.image().colortype() + self.image.colortype() } /// The offset of the directory representing the current image. @@ -871,12 +875,21 @@ impl Decoder { self.current_ifd_pointer } - fn image(&self) -> &Image { - &self.image + /// Loads the IFD at the specified index in the list, if one exists + pub fn seek_to_directory(&mut self, ifd_index: usize) -> TiffResult<()> { + self.seek_to_ifd(ifd_index) } /// Loads the IFD at the specified index in the list, if one exists + /// + /// The directory is then decoded as an image. pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> { + self.seek_to_ifd(ifd_index)?; + self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; + Ok(()) + } + + fn seek_to_ifd(&mut self, ifd_index: usize) -> TiffResult<()> { // Check whether we have seen this IFD before, if so then the index will be less than the // length of the list of ifd offsets If the index is within the list of ifds then we can // load the selected image/IFD. Otherwise we iterate the chain until the selected index. @@ -906,7 +919,6 @@ impl Decoder { } // Reaching this point implies `self.directory` represents the selected ifd. - self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; Ok(()) } @@ -934,7 +946,17 @@ impl Decoder { Ok(()) } + /// Reads the next IFD. + /// + /// 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_directory(&mut self) -> TiffResult<()> { + self.image = Self::no_image(); + self.read_next_ifd_pointer() + } + /// 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<()> { @@ -1094,10 +1116,10 @@ impl Decoder { } fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> { - if expected != self.image().chunk_type { + if expected != self.image.chunk_type { return Err(TiffError::UsageError(UsageError::InvalidChunkType( expected, - self.image().chunk_type, + self.image.chunk_type, ))); } @@ -1106,27 +1128,27 @@ impl Decoder { /// The chunk type (Strips / Tiles) of the image pub fn get_chunk_type(&self) -> ChunkType { - self.image().chunk_type + self.image.chunk_type } /// Number of strips in image pub fn strip_count(&mut self) -> TiffResult { self.check_chunk_type(ChunkType::Strip)?; - let rows_per_strip = self.image().strip_decoder.as_ref().unwrap().rows_per_strip; + let rows_per_strip = self.image.strip_decoder.as_ref().unwrap().rows_per_strip; if rows_per_strip == 0 { return Ok(0); } // rows_per_strip - 1 can never fail since we know it's at least 1 - let height = match self.image().height.checked_add(rows_per_strip - 1) { + let height = match self.image.height.checked_add(rows_per_strip - 1) { Some(h) => h, None => return Err(TiffError::IntSizeError), }; - let strips = match self.image().planar_config { + let strips = match self.image.planar_config { PlanarConfiguration::Chunky => height / rows_per_strip, - PlanarConfiguration::Planar => height / rows_per_strip * self.image().samples as u32, + PlanarConfiguration::Planar => height / rows_per_strip * self.image.samples as u32, }; Ok(strips) @@ -1135,7 +1157,7 @@ impl Decoder { /// Number of tiles in image pub fn tile_count(&mut self) -> TiffResult { self.check_chunk_type(ChunkType::Tile)?; - Ok(u32::try_from(self.image().chunk_offsets.len())?) + Ok(u32::try_from(self.image.chunk_offsets.len())?) } fn read_chunk_to_bytes( @@ -1164,8 +1186,8 @@ impl Decoder { &mut self, chunk_index: u32, ) -> TiffResult { - let data_dims = self.image().chunk_data_dimensions(chunk_index)?; - let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?; + let data_dims = self.image.chunk_data_dimensions(chunk_index)?; + let readout = self.image.readout_for_size(data_dims.0, data_dims.1)?; let extent = readout.result_extent_for_planes(0..1)?; let sample_type = extent.sample_type(); @@ -1180,7 +1202,7 @@ impl Decoder { planes: 1, plane_stride, complete_len: layout.size(), - sample_format: self.image().sample_format, + sample_format: self.image.sample_format, sample_type: Some(sample_type), }) } @@ -1197,15 +1219,15 @@ impl Decoder { &mut self, code_unit: TiffCodingUnit, ) -> TiffResult { - match self.image().planar_config { + match self.image.planar_config { PlanarConfiguration::Chunky => return self.image_chunk_buffer_layout(code_unit.0), PlanarConfiguration::Planar => {} } - let (width, height) = self.image().chunk_data_dimensions(code_unit.0)?; + let (width, height) = self.image.chunk_data_dimensions(code_unit.0)?; let layout = self - .image() + .image .readout_for_size(width, height)? .to_plane_layout()?; @@ -1223,9 +1245,9 @@ impl Decoder { /// /// Note that for planar images each chunk contains only one sample of the underlying data. pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult { - let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; + let (width, height) = self.image.chunk_data_dimensions(chunk_index)?; - let readout = self.image().readout_for_size(width, height)?; + let readout = self.image.readout_for_size(width, height)?; let mut result = readout .result_extent_for_planes(0..1)? @@ -1244,9 +1266,9 @@ impl Decoder { /// /// Note that for planar images each chunk contains only one sample of the underlying data. pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> { - let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; + let (width, height) = self.image.chunk_data_dimensions(chunk_index)?; - let layout = self.image().readout_for_size(width, height)?; + let layout = self.image.readout_for_size(width, height)?; layout.assert_min_layout(buffer)?; self.read_chunk_to_bytes(buffer, chunk_index, &layout)?; @@ -1267,9 +1289,9 @@ impl Decoder { chunk_index: u32, output_width: usize, ) -> TiffResult<()> { - let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; + let (width, height) = self.image.chunk_data_dimensions(chunk_index)?; - let mut layout = self.image().readout_for_size(width, height)?; + let mut layout = self.image.readout_for_size(width, height)?; layout.set_row_stride(output_width)?; let extent = layout.result_extent_for_planes(0..1)?; @@ -1299,8 +1321,8 @@ impl Decoder { slice: TiffCodingUnit, buffer: &mut [u8], ) -> TiffResult<()> { - let (width, height) = self.image().chunk_data_dimensions(slice.0)?; - let readout = self.image().readout_for_size(width, height)?; + let (width, height) = self.image.chunk_data_dimensions(slice.0)?; + let readout = self.image.readout_for_size(width, height)?; let ref layout @ image::PlaneLayout { ref plane_offsets, @@ -1322,7 +1344,7 @@ impl Decoder { for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() { let chunk = slice.0 + idx as u32 * readout.chunks_per_plane; - self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?; + self.goto_offset_u64(self.image.chunk_offsets[chunk as usize])?; self.image.expand_chunk( &mut self.value_reader, @@ -1338,13 +1360,13 @@ impl Decoder { /// Returns the default chunk size for the current image. Any given chunk in the image is at most as large as /// the value returned here. For the size of the data (chunk minus padding), use `chunk_data_dimensions`. pub fn chunk_dimensions(&self) -> (u32, u32) { - self.image().chunk_dimensions().unwrap() + self.image.chunk_dimensions().unwrap() } /// Returns the size of the data in the chunk with the specified index. This is the default size of the chunk, /// minus any padding. pub fn chunk_data_dimensions(&self, chunk_index: u32) -> (u32, u32) { - self.image() + self.image .chunk_data_dimensions(chunk_index) .expect("invalid chunk_index") } @@ -1363,7 +1385,7 @@ impl Decoder { /// indicate the layout needed to read the first data plane. This will be fixed in a future /// major version of `tiff`. pub fn image_buffer_layout(&mut self) -> TiffResult { - let layout = self.image().readout_for_image()?.to_plane_layout()?; + let layout = self.image.readout_for_image()?.to_plane_layout()?; Ok(BufferLayoutPreference::from_planes(&layout)) } @@ -1387,7 +1409,7 @@ impl Decoder { /// replaced, to ensure that existing code will not run into unexpectedly large allocations /// that will error on limits instead. pub fn read_image(&mut self) -> TiffResult { - let readout = self.image().readout_for_image()?; + let readout = self.image.readout_for_image()?; let mut result = readout .result_extent_for_planes(0..1)? @@ -1448,7 +1470,7 @@ impl Decoder { &mut self, result: &mut DecodingResult, ) -> TiffResult { - let readout = self.image().readout_for_image()?; + let readout = self.image.readout_for_image()?; let planes = readout.to_plane_layout()?; let num_planes = if planes.total_bytes <= self.value_reader.limits.decoding_buffer_size { @@ -1479,7 +1501,7 @@ impl Decoder { /// Returns an error if the buffer fits less than one plane. In particular, for non-planar /// images returns an error if the buffer does not fit the required size. pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { - let readout = self.image().readout_for_image()?; + let readout = self.image.readout_for_image()?; let ref layout @ image::PlaneLayout { ref plane_offsets, @@ -1503,7 +1525,7 @@ impl Decoder { for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() { let chunk = chunk + idx as u32 * readout.chunks_per_plane; - self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?; + self.goto_offset_u64(self.image.chunk_offsets[chunk as usize])?; self.image.expand_chunk( &mut self.value_reader, From 647e9b4a8165409ee51d19753290d9b73707ab19 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:58:36 +0100 Subject: [PATCH 4/7] Add TiffHeader to open a file without seeking --- src/decoder/image.rs | 24 +++++++ src/decoder/mod.rs | 167 ++++++++++++++++++++++++++++++++----------- src/tags.rs | 9 +++ 3 files changed, 159 insertions(+), 41 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index b0512d6..5077f68 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -146,6 +146,30 @@ pub(crate) struct ReadoutLayout { } impl Image { + pub(crate) fn no_image() -> Image { + Image { + width: 0, + height: 0, + bits_per_sample: 1, + samples: 1, + extra_samples: vec![], + photometric_samples: 1, + sample_format: SampleFormat::Uint, + photometric_interpretation: PhotometricInterpretation::BlackIsZero, + compression_method: CompressionMethod::None, + jpeg_tables: None, + predictor: Predictor::None, + chunk_type: ChunkType::Strip, + planar_config: PlanarConfiguration::Chunky, + strip_decoder: None, + tile_attributes: None, + chunk_offsets: Vec::new(), + chunk_bytes: Vec::new(), + chroma_subsampling: (2, 2), + decompression_to_host_endian: false, + } + } + pub fn from_reader( decoder: &mut ValueReader, ifd: &Directory, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 3968482..8c7d2ed 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -4,8 +4,8 @@ use std::io::{self, Read, Seek}; use std::num::NonZeroUsize; use crate::tags::{ - CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, - SampleFormat, Tag, Type, ValueBuffer, + ByteOrder, IfdPointer, PlanarConfiguration, Predictor, SampleFormat, Tag, TiffVariant, Type, + ValueBuffer, }; use crate::{ bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, @@ -14,7 +14,7 @@ use crate::{ use half::f16; use self::image::Image; -use self::stream::{ByteOrder, EndianReader}; +use self::stream::EndianReader; mod cycles; pub mod ifd; @@ -505,6 +505,14 @@ impl Default for Limits { } } +/// Describe the header of TIFF file, including where to find the first image directory. +#[derive(Debug)] +pub struct TiffHeader { + pub byte_order: ByteOrder, + pub variant: TiffVariant, + pub first_ifd: IfdPointer, +} + /// The representation of a TIFF decoder /// /// Currently does not support decoding of interlaced images @@ -767,8 +775,8 @@ fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) { host.convert_endian_bytes(class, buf, byte_order); } -impl Decoder { - pub fn new(mut r: R) -> TiffResult> { +impl TiffHeader { + pub fn parse(mut r: R) -> TiffResult { let mut endianess = Vec::with_capacity(2); (&mut r).take(2).read_to_end(&mut endianess)?; let byte_order = match &*endianess { @@ -780,6 +788,7 @@ impl Decoder { )) } }; + let mut reader = EndianReader::new(r, byte_order); let bigtiff = match reader.read_u16()? { @@ -806,57 +815,69 @@ impl Decoder { } }; - let next_ifd = if bigtiff { - Some(reader.read_u64()?) + let next_ifd = IfdPointer(if bigtiff { + reader.read_u64()? } else { - Some(u64::from(reader.read_u32()?)) - } - .map(IfdPointer); + u64::from(reader.read_u32()?) + }); - let current_ifd = *next_ifd.as_ref().unwrap(); - let ifd_offsets = vec![current_ifd]; + Ok(TiffHeader { + byte_order, + variant: if bigtiff { + TiffVariant::BigTiff + } else { + TiffVariant::Standard + }, + first_ifd: next_ifd, + }) + } + + /// Open a file, without seeking to any IFD yet. + pub fn open(&self, r: R) -> Decoder { + let reader = EndianReader::new(r, self.byte_order); + + Decoder { + value_reader: ValueReader { + reader, + bigtiff: matches!(self.variant, TiffVariant::BigTiff), + limits: Default::default(), + }, + next_ifd_pointer: Some(self.first_ifd), + ifd_offsets: vec![], + current_ifd_pointer: None, + directory: Directory::empty(), + seen_ifds: cycles::IfdCycles::new(), + image: Image::no_image(), + } + } +} + +impl Decoder { + pub fn new(mut r: R) -> TiffResult> { + let header = TiffHeader::parse(&mut r)?; + let reader = EndianReader::new(r, header.byte_order); + + let first_ifd = header.first_ifd; + let ifd_offsets = vec![first_ifd]; let mut decoder = Decoder { value_reader: ValueReader { reader, - bigtiff, + bigtiff: matches!(header.variant, TiffVariant::BigTiff), limits: Default::default(), }, - next_ifd_pointer: next_ifd, + next_ifd_pointer: Some(first_ifd), ifd_offsets, current_ifd_pointer: None, directory: Directory::empty(), seen_ifds: cycles::IfdCycles::new(), - image: Self::no_image(), + image: Image::no_image(), }; + decoder.next_image()?; Ok(decoder) } - fn no_image() -> Image { - Image { - width: 0, - height: 0, - bits_per_sample: 1, - samples: 1, - extra_samples: vec![], - photometric_samples: 1, - sample_format: SampleFormat::Uint, - photometric_interpretation: PhotometricInterpretation::BlackIsZero, - compression_method: CompressionMethod::None, - jpeg_tables: None, - predictor: Predictor::None, - chunk_type: ChunkType::Strip, - planar_config: PlanarConfiguration::Chunky, - strip_decoder: None, - tile_attributes: None, - chunk_offsets: Vec::new(), - chunk_bytes: Vec::new(), - chroma_subsampling: (2, 2), - decompression_to_host_endian: false, - } - } - pub fn with_limits(mut self, limits: Limits) -> Decoder { self.value_reader.limits = limits; self @@ -951,7 +972,7 @@ impl Decoder { /// 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_directory(&mut self) -> TiffResult<()> { - self.image = Self::no_image(); + self.image = Image::no_image(); self.read_next_ifd_pointer() } @@ -960,6 +981,7 @@ impl Decoder { /// 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<()> { + self.image = Image::no_image(); self.read_next_ifd_pointer()?; self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; Ok(()) @@ -1911,7 +1933,7 @@ impl<'l> IfdDecoder<'l> { mod tests { use super::Decoder; use crate::{ - bytecast, + bytecast, encoder, tags::{ByteOrder, Tag, ValueBuffer}, }; @@ -1972,4 +1994,67 @@ mod tests { assert_eq!(&by_bytes[..3], &[8, 8, 8]); } } + + #[test] + fn non_image_directory_between() { + let mut cursor = std::io::Cursor::new(vec![]); + + { + let mut encoder = encoder::TiffEncoder::new(&mut cursor).unwrap(); + encoder + .write_image::(1, 1, &[0]) + .unwrap(); + encoder.image_directory().unwrap().finish().unwrap(); + encoder + .write_image::(1, 1, &[0]) + .unwrap(); + } + + cursor.set_position(0); + + let mut decoder = Decoder::new(cursor).unwrap(); + let first_ifd = decoder.image_ifd(); + assert!(first_ifd.directory().get(Tag::ImageWidth).is_some()); + assert!(decoder.more_images()); + + assert!(decoder.next_image().is_err()); + let second_ifd = decoder.image_ifd(); + assert!(second_ifd.directory().get(Tag::ImageWidth).is_none()); + assert!(decoder.more_images()); + + assert!(decoder.next_image().is_ok()); + let third_ifd = decoder.image_ifd(); + assert!(third_ifd.directory().get(Tag::ImageWidth).is_some()); + assert!(!decoder.more_images()); + } + + #[test] + fn non_image_directory_start() { + let mut cursor = std::io::Cursor::new(vec![]); + + { + let mut encoder = encoder::TiffEncoder::new(&mut cursor).unwrap(); + encoder.image_directory().unwrap().finish().unwrap(); + encoder + .write_image::(1, 1, &[0]) + .unwrap(); + } + + cursor.set_position(0); + + let header = super::TiffHeader::parse(&mut cursor).unwrap(); + let mut decoder = header.open(cursor); + assert!(decoder.more_images()); + + assert_eq!(decoder.ifd_pointer(), None); + assert!(decoder.next_directory().is_ok()); + let first_ifd = decoder.image_ifd(); + assert!(first_ifd.directory().get(Tag::ImageWidth).is_none()); + assert!(decoder.more_images()); + + assert!(decoder.next_image().is_ok()); + let second_ifd = decoder.image_ifd(); + assert!(second_ifd.directory().get(Tag::ImageWidth).is_some()); + assert!(!decoder.more_images()); + } } diff --git a/src/tags.rs b/src/tags.rs index 690c30c..b66cc73 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -589,3 +589,12 @@ pub(crate) enum EndianBytes { Four, Eight, } + +/// Defines how directories are laid out in the file. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TiffVariant { + /// TIFF-V6.0 with 32-bit offsets and counts. + Standard, + /// Extended TIFF with 64-bit offsets and counts and long directories. + BigTiff, +} From e093e15bbecc24b6aa0d9669ae97f62fed9d7568 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:44:08 +0100 Subject: [PATCH 5/7] Remove redundant next_ifd attribute --- src/decoder/mod.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8c7d2ed..bccc398 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -525,7 +525,6 @@ where /// borrow the stream access and the other fields mutably at the same time. value_reader: ValueReader, current_ifd_pointer: Option, - next_ifd_pointer: Option, directory: Directory, /// The IFDs we visited already in this chain of IFDs. ifd_offsets: Vec, @@ -842,10 +841,13 @@ impl TiffHeader { bigtiff: matches!(self.variant, TiffVariant::BigTiff), limits: Default::default(), }, - next_ifd_pointer: Some(self.first_ifd), ifd_offsets: vec![], current_ifd_pointer: None, - directory: Directory::empty(), + directory: { + let mut dir = Directory::empty(); + dir.set_next(Some(self.first_ifd)); + dir + }, seen_ifds: cycles::IfdCycles::new(), image: Image::no_image(), } @@ -866,10 +868,13 @@ impl Decoder { bigtiff: matches!(header.variant, TiffVariant::BigTiff), limits: Default::default(), }, - next_ifd_pointer: Some(first_ifd), ifd_offsets, current_ifd_pointer: None, - directory: Directory::empty(), + directory: { + let mut dir = Directory::empty(); + dir.set_next(Some(first_ifd)); + dir + }, seen_ifds: cycles::IfdCycles::new(), image: Image::no_image(), }; @@ -917,7 +922,6 @@ impl Decoder { if let Some(&ifd_offset) = self.ifd_offsets.get(ifd_index) { self.directory = self.value_reader.read_directory(ifd_offset)?; self.current_ifd_pointer = Some(ifd_offset); - self.next_ifd_pointer = self.directory.next(); } else { // We do not know if we are at the last IFD in the chain if we previous did seek back. // The `read_next_ifd_pointer` correctly handles this. @@ -944,7 +948,7 @@ impl Decoder { } fn read_next_ifd_pointer(&mut self) -> TiffResult<()> { - let Some(next_ifd) = self.next_ifd_pointer.take() else { + let Some(next_ifd) = self.directory.next() else { return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, )); @@ -961,7 +965,6 @@ impl Decoder { } self.current_ifd_pointer = Some(next_ifd); - self.next_ifd_pointer = ifd.next(); self.directory = ifd; Ok(()) @@ -989,7 +992,7 @@ impl Decoder { /// Returns `true` if there is at least one more image available. pub fn more_images(&self) -> bool { - self.next_ifd_pointer.is_some() + self.directory.next().is_some() } /// Returns the byte_order of the file. From c7b97519a07688e94928eab8995b89358f3b2792 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 6 Mar 2026 04:42:02 +0100 Subject: [PATCH 6/7] Error guarantee on decoder IFD iteration --- src/decoder/mod.rs | 140 +++++++++++++++++++++++++++++++++------------ 1 file changed, 102 insertions(+), 38 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index bccc398..4d87ccf 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -902,53 +902,66 @@ impl Decoder { } /// Loads the IFD at the specified index in the list, if one exists + /// + /// Error guarantee: the current directory and its offset is not modified when an error occurs. pub fn seek_to_directory(&mut self, ifd_index: usize) -> TiffResult<()> { - self.seek_to_ifd(ifd_index) + let (offset, directory) = self.find_nth_ifd(ifd_index)?; + self.image = Image::no_image(); + self.current_ifd_pointer = Some(offset); + self.directory = directory; + Ok(()) } /// Loads the IFD at the specified index in the list, if one exists /// /// The directory is then decoded as an image. + /// + /// Error guarantee: the current directory and its offset is not modified when an error occurs. pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> { - self.seek_to_ifd(ifd_index)?; - self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; + let (offset, directory) = self.find_nth_ifd(ifd_index)?; + self.image = Image::from_reader(&mut self.value_reader, &directory)?; + self.current_ifd_pointer = Some(offset); + self.directory = directory; Ok(()) } - fn seek_to_ifd(&mut self, ifd_index: usize) -> TiffResult<()> { + /// Change the directory to the nth in the linked-list chain of IFDs. + fn find_nth_ifd(&mut self, ifd_index: usize) -> TiffResult<(IfdPointer, Directory)> { // Check whether we have seen this IFD before, if so then the index will be less than the // length of the list of ifd offsets If the index is within the list of ifds then we can // load the selected image/IFD. Otherwise we iterate the chain until the selected index. if let Some(&ifd_offset) = self.ifd_offsets.get(ifd_index) { - self.directory = self.value_reader.read_directory(ifd_offset)?; - self.current_ifd_pointer = Some(ifd_offset); + let directory = self.value_reader.read_directory(ifd_offset)?; + Ok((ifd_offset, directory)) } else { // We do not know if we are at the last IFD in the chain if we previous did seek back. // The `read_next_ifd_pointer` correctly handles this. + let mut chained_ifd = self.current_ifd_pointer; + let mut next_ifd = self.directory.next(); + loop { // Follow the list until we find the one we want, or we reach the end, whichever happens first - self.read_next_ifd_pointer()?; - - if self.directory.next().is_none() { - break; - } + let (ifd_pointer, ifd) = self.read_chained_ifd(chained_ifd, next_ifd)?; + chained_ifd = Some(ifd_pointer); + next_ifd = ifd.next(); if ifd_index < self.ifd_offsets.len() { // We only end up here if initially `ifd_index >= self.ifd_offsets.len()` and // each loop iteration increases the length of `self.ifd_offsets` by (at most) // one. debug_assert!(ifd_index + 1 == self.ifd_offsets.len()); - break; + return Ok((ifd_pointer, ifd)); } } } - - // Reaching this point implies `self.directory` represents the selected ifd. - Ok(()) } - fn read_next_ifd_pointer(&mut self) -> TiffResult<()> { - let Some(next_ifd) = self.directory.next() else { + fn read_chained_ifd( + &mut self, + current_ifd_pointer: Option, + next_ifd: Option, + ) -> TiffResult<(IfdPointer, Directory)> { + let Some(next_ifd) = next_ifd else { return Err(TiffError::FormatError( TiffFormatError::ImageFileDirectoryNotFound, )); @@ -960,33 +973,40 @@ impl Decoder { 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_pointer { + if self.ifd_offsets.last().copied() == current_ifd_pointer { self.ifd_offsets.push(next_ifd); } - self.current_ifd_pointer = Some(next_ifd); - self.directory = ifd; - - Ok(()) + Ok((next_ifd, ifd)) } /// Reads the next IFD. /// /// 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. + /// + /// Error guarantee: the current directory and its offset is not modified when an error occurs. pub fn next_directory(&mut self) -> TiffResult<()> { + let next_ifd = self.directory.next(); + let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?; self.image = Image::no_image(); - self.read_next_ifd_pointer() + self.current_ifd_pointer = Some(offset); + self.directory = directory; + Ok(()) } /// 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. + /// + /// Error guarantee: the current directory and its offset is not modified when an error occurs. pub fn next_image(&mut self) -> TiffResult<()> { - self.image = Image::no_image(); - self.read_next_ifd_pointer()?; - self.image = Image::from_reader(&mut self.value_reader, &self.directory)?; + let next_ifd = self.directory.next(); + let (offset, directory) = self.read_chained_ifd(self.current_ifd_pointer, next_ifd)?; + self.image = Image::from_reader(&mut self.value_reader, &directory)?; + self.current_ifd_pointer = Some(offset); + self.directory = directory; Ok(()) } @@ -2014,21 +2034,65 @@ mod tests { } cursor.set_position(0); + let (first_ptr, second_ptr, third_ptr); - let mut decoder = Decoder::new(cursor).unwrap(); - let first_ifd = decoder.image_ifd(); - assert!(first_ifd.directory().get(Tag::ImageWidth).is_some()); - assert!(decoder.more_images()); + { + let mut decoder = Decoder::new(&mut cursor).unwrap(); + first_ptr = decoder.ifd_pointer(); + let first_ifd = decoder.image_ifd(); + assert!(first_ifd.directory().get(Tag::ImageWidth).is_some()); + assert!(decoder.more_images()); - assert!(decoder.next_image().is_err()); - let second_ifd = decoder.image_ifd(); - assert!(second_ifd.directory().get(Tag::ImageWidth).is_none()); - assert!(decoder.more_images()); + assert!(decoder.next_image().is_err()); + let broken_ifd = decoder.image_ifd(); + assert!(broken_ifd.directory().get(Tag::ImageWidth).is_some()); - assert!(decoder.next_image().is_ok()); - let third_ifd = decoder.image_ifd(); - assert!(third_ifd.directory().get(Tag::ImageWidth).is_some()); - assert!(!decoder.more_images()); + assert!(decoder.next_directory().is_ok()); + second_ptr = decoder.ifd_pointer(); + let second_ifd = decoder.image_ifd(); + assert!(second_ifd.directory().get(Tag::ImageWidth).is_none()); + assert!(decoder.more_images()); + + assert!(decoder.next_image().is_ok()); + third_ptr = decoder.ifd_pointer(); + let third_ifd = decoder.image_ifd(); + assert!(third_ifd.directory().get(Tag::ImageWidth).is_some()); + assert!(!decoder.more_images()); + + assert!(decoder.seek_to_image(1).is_err()); + assert_eq!(decoder.ifd_pointer(), third_ptr); + + assert!(decoder.seek_to_directory(1).is_ok()); + assert_eq!(decoder.ifd_pointer(), second_ptr); + } + + cursor.set_position(0); + + { + let mut seek_immediately = Decoder::new(&mut cursor).unwrap(); + assert_eq!(seek_immediately.ifd_pointer(), first_ptr); + assert!(seek_immediately.seek_to_image(1).is_err()); + assert!(seek_immediately.seek_to_image(2).is_ok()); + assert_eq!(seek_immediately.ifd_pointer(), third_ptr); + assert!(seek_immediately + .image_ifd() + .directory() + .get(Tag::ImageWidth) + .is_some()); + } + + cursor.set_position(0); + + { + let mut seek_immediately = Decoder::new(&mut cursor).unwrap(); + assert!(seek_immediately.seek_to_directory(1).is_ok()); + assert_eq!(seek_immediately.ifd_pointer(), second_ptr); + assert!(seek_immediately + .image_ifd() + .directory() + .get(Tag::ImageWidth) + .is_none()); + } } #[test] From 08f1b25fac00b6eb710eefc2e7b9da4ec4ff8eca Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:35:45 +0100 Subject: [PATCH 7/7] Drafting release notes --- CHANGES.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 28b7186..5acf668 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,16 @@ +# Version 0.12.0 (unreleased) + +Additions: +- Added the `TiffHeader` type. It can be used to create a `Decoder` in a state + before the first IFD and allows opening files at an arbitrary first IFD + offset rather than one indicated by an encoded header at the start. + +Changes: +- The IFD iteration methods on `Decoder` (`next_image`, `seek_to_image`, etc) + now come with a strong error guarantee. They will not modify the current + directory state if they fail. This includes the failure to interpret an IFD + as a supported image in the case of `*_image` variants. + # Version 0.11.3 Additions: