From 6b04bff6b234dba0c9763e5cd24aa1b6d5019e58 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:05:05 +0100 Subject: [PATCH] Relax Decoder, new interface documentation The bounds `Read + Seek` are not representational so we may remove them from the struct itself. This also allows us to use more precise bounds for direct access to the inner stream, especially were that does not seek. Groups operations according to their use case. --- src/decoder/mod.rs | 226 ++++++++++++++++++--------------- tests/decode_bigtiff_images.rs | 11 +- tests/decode_geotiff_images.rs | 14 +- tests/decode_images.rs | 12 +- tests/encode_images.rs | 51 ++++++-- 5 files changed, 182 insertions(+), 132 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c9f895f..7b709d0 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -229,12 +229,19 @@ pub struct TiffHeader { /// The representation of a TIFF decoder /// -/// Currently does not support decoding of interlaced images +/// There are three groups of operations: +/// +/// - [Manipulate the directory state](#header-manipulate-directory) to which the decoder currently +/// refers in the file. +/// - [Interact with the underlying stream](#header-stream-access) instead of entries in an IFD. +/// - [Access the encoded image](#header-image). +/// +/// ## Limitations +/// +/// Currently does not support decoding of interlaced images. It also does not support images with +/// downsampling in any direction; except if they are also JPEG or WebP compressed. #[derive(Debug)] -pub struct Decoder -where - R: Read + Seek, -{ +pub struct Decoder { /// 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, @@ -622,6 +629,7 @@ impl TiffHeader { } } +/// Methods to manipulate the directory state impl Decoder { /// Parse the image header from the underlying file and create a decoder positioned before its /// first directory. @@ -699,29 +707,25 @@ impl Decoder { self } - pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> { - Ok((self.image.width, self.image.height)) - } - - pub fn colortype(&mut self) -> TiffResult { - self.image.colortype() - } - - /// Returns the color map (palette) for the current image, if it uses - /// `PhotometricInterpretation::RGBPalette`. - /// - /// The returned slice contains `3 * 2^BitsPerSample` entries as u16 values, - /// laid out as: all red values, then all green values, then all blue values. - /// Each value is in the range `0..=65535` regardless of the actual bit depth. - pub fn color_map(&self) -> Option<&[u16]> { - self.image.color_map.as_deref() - } - /// The offset of the directory representing the current image. pub fn ifd_pointer(&mut self) -> Option { self.current_ifd_pointer } + /// Get the IFD decoder for our current IFD. + /// + /// This may be used with any directory, valid image or otherwise. When the decoder has not + /// read a directory yet (a state after [`TiffHeader::open`]) the decoder will represent an + /// empty directory with only the next pointer set. + pub fn current_ifd(&mut self) -> IfdDecoder<'_> { + IfdDecoder { + inner: tag_reader::TagReader { + decoder: &mut self.value_reader, + ifd: &self.directory, + }, + } + } + /// Loads the IFD at the specified index in the list, if one exists /// /// Error guarantee: the current directory and [`Self::ifd_pointer`] is not modified when an @@ -835,6 +839,64 @@ impl Decoder { Ok(()) } + /// Read a tag-entry map from a known offset. + /// + /// 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) + } + + /// Prepare reading values for tags of a 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::open(&mut data)?; + /// + /// decoder.next_directory()?; + /// 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.read_directory_tags(&subdir).find_tag(Tag::SubfileType)?; + /// // omitted: handle the subfiles, e.g. thumbnails + /// } + /// + /// # Ok::<_, tiff::TiffError>(()) + /// ``` + pub fn read_directory_tags<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { + IfdDecoder { + inner: tag_reader::TagReader { + decoder: &mut self.value_reader, + ifd, + }, + } + } + /// Returns `true` if there is at least one more image available. pub fn more_images(&self) -> bool { self.directory.next().is_some() @@ -849,6 +911,14 @@ impl Decoder { pub fn byte_order(&self) -> ByteOrder { self.value_reader.reader.byte_order } +} + +/// Methods that interact with the underlying reader. +impl Decoder { + /// Returns a mutable reference to the stream being decoded. + pub fn inner(&mut self) -> &mut R { + self.value_reader.reader.inner() + } #[inline] pub fn read_ifd_offset(&mut self) -> Result { @@ -859,11 +929,6 @@ impl Decoder { } } - /// Returns a mutable reference to the stream being decoded. - pub fn inner(&mut self) -> &mut R { - self.value_reader.reader.inner() - } - /// Reads a TIFF byte value #[inline] pub fn read_byte(&mut self) -> Result { @@ -953,38 +1018,24 @@ impl Decoder { /// Moves the cursor to the specified offset #[inline] - pub fn goto_offset(&mut self, offset: u32) -> io::Result<()> { + pub fn goto_offset(&mut self, offset: u32) -> io::Result<()> + where + R: Seek, + { self.goto_offset_u64(offset.into()) } #[inline] - pub fn goto_offset_u64(&mut self, offset: u64) -> io::Result<()> { + pub fn goto_offset_u64(&mut self, offset: u64) -> io::Result<()> + where + R: Seek, + { self.value_reader.reader.goto_offset(offset) } +} - /// Read a tag-entry map from a known offset. - /// - /// 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) - } - +/// Methods that interact with the current directory as an image. +impl Decoder { fn check_chunk_type(&self, expected: ChunkType) -> TiffResult<()> { if expected != self.image.chunk_type { return Err(TiffError::UsageError(UsageError::InvalidChunkType( @@ -996,6 +1047,24 @@ impl Decoder { Ok(()) } + pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> { + Ok((self.image.width, self.image.height)) + } + + pub fn colortype(&mut self) -> TiffResult { + self.image.colortype() + } + + /// Returns the color map (palette) for the current image, if it uses + /// `PhotometricInterpretation::RGBPalette`. + /// + /// The returned slice contains `3 * 2^BitsPerSample` entries as u16 values, + /// laid out as: all red values, then all green values, then all blue values. + /// Each value is in the range `0..=65535` regardless of the actual bit depth. + pub fn color_map(&self) -> Option<&[u16]> { + self.image.color_map.as_deref() + } + /// The chunk type (Strips / Tiles) of the image pub fn get_chunk_type(&self) -> ChunkType { self.image.chunk_type @@ -1409,58 +1478,9 @@ impl Decoder { Ok(()) } - - /// Get the IFD decoder for our current IFD. - /// - /// This may be used with any directory, valid image or otherwise. When the decoder has not - /// read a directory yet (a state after [`TiffHeader::open`]) the decoder will represent an - /// empty directory with only the next pointer set. - pub fn current_ifd(&mut self) -> IfdDecoder<'_> { - IfdDecoder { - inner: tag_reader::TagReader { - decoder: &mut self.value_reader, - ifd: &self.directory, - }, - } - } - - /// Prepare reading values for tags of a 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::open(&mut data)?; - /// - /// decoder.next_directory()?; - /// 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.read_directory_tags(&subdir).find_tag(Tag::SubfileType)?; - /// // omitted: handle the subfiles, e.g. thumbnails - /// } - /// - /// # Ok::<_, tiff::TiffError>(()) - /// ``` - pub fn read_directory_tags<'ifd>(&'ifd mut self, ifd: &'ifd Directory) -> IfdDecoder<'ifd> { - IfdDecoder { - inner: tag_reader::TagReader { - decoder: &mut self.value_reader, - ifd, - }, - } - } } -#[doc(hidden = "Deprecated methods")] +#[doc(hidden)] #[allow(deprecated)] impl Decoder { /// Tries to retrieve a tag from the current image directory. diff --git a/tests/decode_bigtiff_images.rs b/tests/decode_bigtiff_images.rs index 127e18c..3a51009 100644 --- a/tests/decode_bigtiff_images.rs +++ b/tests/decode_bigtiff_images.rs @@ -26,21 +26,20 @@ fn test_big_tiff() { decoder.colortype().expect("Cannot get colortype"), ColorType::RGB(8) ); + + let mut ifd = decoder.current_ifd(); assert_eq!( - decoder - .get_tag_u64(Tag::StripOffsets) + ifd.get_tag_u64(Tag::StripOffsets) .expect("Cannot get StripOffsets"), 16 ); assert_eq!( - decoder - .get_tag_u64(Tag::RowsPerStrip) + ifd.get_tag_u64(Tag::RowsPerStrip) .expect("Cannot get RowsPerStrip"), 64 ); assert_eq!( - decoder - .get_tag_u64(Tag::StripByteCounts) + ifd.get_tag_u64(Tag::StripByteCounts) .expect("Cannot get StripByteCounts"), 12288 ) diff --git a/tests/decode_geotiff_images.rs b/tests/decode_geotiff_images.rs index b40aff2..e582dbd 100644 --- a/tests/decode_geotiff_images.rs +++ b/tests/decode_geotiff_images.rs @@ -30,27 +30,25 @@ fn test_geo_tiff() { num_samples: 5 } ); + + let mut ifd = decoder.current_ifd(); assert_eq!( - decoder - .get_tag_u64(Tag::StripOffsets) + ifd.get_tag_u64(Tag::StripOffsets) .expect("Cannot get StripOffsets"), 418 ); assert_eq!( - decoder - .get_tag_u64(Tag::RowsPerStrip) + ifd.get_tag_u64(Tag::RowsPerStrip) .expect("Cannot get RowsPerStrip"), 10 ); assert_eq!( - decoder - .get_tag_u64(Tag::StripByteCounts) + ifd.get_tag_u64(Tag::StripByteCounts) .expect("Cannot get StripByteCounts"), 1000 ); assert_eq!( - decoder - .get_tag(Tag::ModelPixelScaleTag) + ifd.get_tag(Tag::ModelPixelScaleTag) .expect("Cannot get pixel scale") .into_f64_vec() .expect("Cannot get pixel scale"), diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 213be5a..a68380a 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -404,7 +404,10 @@ fn test_string_tags() { let img_file = File::open(path).expect("Cannot find test image!"); let mut decoder = Decoder::open(img_file).expect("Cannot create decoder"); decoder.next_image().expect("Cannot read image IFD"); - let software = decoder.get_tag(tiff::tags::Tag::Software).unwrap(); + let software = decoder + .current_ifd() + .get_tag(tiff::tags::Tag::Software) + .unwrap(); match software { ifd::Value::Ascii(s) => assert_eq!( &s, @@ -478,7 +481,8 @@ fn test_inner_access() { decoder.next_image().expect("Cannot read image IFD"); assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8)); - let c = decoder + let mut ifd = decoder.current_ifd(); + let c = ifd .get_tag(tiff::tags::Tag::Compression) .unwrap() .into_u16() @@ -487,13 +491,13 @@ fn test_inner_access() { // Because the image is uncompressed, reading the first tile directly with the inner reader // should yield the same result as reading it with the decoder's read_chunk method. - let first_offset = decoder + let first_offset = ifd .get_tag_u32_vec(tiff::tags::Tag::TileOffsets) .unwrap() .into_iter() .next() .unwrap(); - let first_byte_count = decoder + let first_byte_count = ifd .get_tag_u32_vec(tiff::tags::Tag::TileByteCounts) .unwrap() .into_iter() diff --git a/tests/encode_images.rs b/tests/encode_images.rs index 63aae78..3790cb5 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -40,6 +40,7 @@ fn encode_decode() { assert_eq!(decoder.dimensions().unwrap(), (100, 100)); let mut all_tags = decoder + .current_ifd() .tag_iter() .filter_map(Result::ok) .collect::>(); @@ -80,7 +81,7 @@ fn encode_decode() { ); assert_eq!( - decoder.get_tag(Tag::Artist).unwrap(), + decoder.current_ifd().get_tag(Tag::Artist).unwrap(), ifd::Value::Ascii("Image-tiff".into()) ); @@ -125,6 +126,7 @@ fn encode_decode_big() { assert_eq!(decoder.dimensions().unwrap(), (100, 100)); let mut all_tags = decoder + .current_ifd() .tag_iter() .filter_map(Result::ok) .collect::>(); @@ -165,7 +167,7 @@ fn encode_decode_big() { ); assert_eq!( - decoder.get_tag(Tag::Artist).unwrap(), + decoder.current_ifd().get_tag(Tag::Artist).unwrap(), ifd::Value::Ascii("Image-tiff".into()) ); let mut buffer = tiff::decoder::DecodingSampleBuffer::U8(vec![]); @@ -539,37 +541,57 @@ trait AssertDecode { impl AssertDecode for Decoder { fn assert_tag_u32(&mut self, tag: u16) -> u32 { - self.get_tag(Tag::Unknown(tag)).unwrap().into_u32().unwrap() + self.current_ifd() + .get_tag(Tag::Unknown(tag)) + .unwrap() + .into_u32() + .unwrap() } fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec { - self.get_tag(Tag::Unknown(tag)) + self.current_ifd() + .get_tag(Tag::Unknown(tag)) .unwrap() .into_u32_vec() .unwrap() } fn assert_tag_i32(&mut self, tag: u16) -> i32 { - self.get_tag(Tag::Unknown(tag)).unwrap().into_i32().unwrap() + self.current_ifd() + .get_tag(Tag::Unknown(tag)) + .unwrap() + .into_i32() + .unwrap() } fn assert_tag_i32_vec(&mut self, tag: u16) -> Vec { - self.get_tag(Tag::Unknown(tag)) + self.current_ifd() + .get_tag(Tag::Unknown(tag)) .unwrap() .into_i32_vec() .unwrap() } fn assert_tag_u64(&mut self, tag: u16) -> u64 { - self.get_tag(Tag::Unknown(tag)).unwrap().into_u64().unwrap() + self.current_ifd() + .get_tag(Tag::Unknown(tag)) + .unwrap() + .into_u64() + .unwrap() } fn assert_tag_u64_vec(&mut self, tag: u16) -> Vec { - self.get_tag(Tag::Unknown(tag)) + self.current_ifd() + .get_tag(Tag::Unknown(tag)) .unwrap() .into_u64_vec() .unwrap() } fn assert_tag_i64(&mut self, tag: u16) -> i64 { - self.get_tag(Tag::Unknown(tag)).unwrap().into_i64().unwrap() + self.current_ifd() + .get_tag(Tag::Unknown(tag)) + .unwrap() + .into_i64() + .unwrap() } fn assert_tag_i64_vec(&mut self, tag: u16) -> Vec { - self.get_tag(Tag::Unknown(tag)) + self.current_ifd() + .get_tag(Tag::Unknown(tag)) .unwrap() .into_i64_vec() .unwrap() @@ -776,7 +798,13 @@ fn test_rows_per_strip() { { let mut decoder = Decoder::open(&mut file).unwrap(); decoder.next_image().unwrap(); - assert_eq!(decoder.get_tag_u64(Tag::RowsPerStrip).unwrap(), 2); + assert_eq!( + decoder + .current_ifd() + .get_tag_u64(Tag::RowsPerStrip) + .unwrap(), + 2 + ); assert_eq!(decoder.strip_count().unwrap(), 50); for i in 0..50 { @@ -829,6 +857,7 @@ fn test_auxiliary_directory() { let _ = decoder.read_image().unwrap(); let exif_location = decoder + .current_ifd() .get_tag(Tag::ExifDirectory) .expect("second directory missing Exif") .into_ifd_pointer()