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.
This commit is contained in:
A. Molzer committed 2026-03-31 09:05:43 +02:00
1 parent bd0dccef3a
commit 6b04bff6b2
5 files changed
+181 -131

No files matched your search

+122 -102
View File
@@ -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<R>
where
R: Read + Seek,
{
pub struct Decoder<R> {
/// 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<R>,
@@ -622,6 +629,7 @@ impl TiffHeader {
}
}
/// <span id=header-manipulate-directory>Methods to manipulate the directory state</span>
impl<R: Read + Seek> Decoder<R> {
/// Parse the image header from the underlying file and create a decoder positioned before its
/// first directory.
@@ -699,29 +707,25 @@ impl<R: Read + Seek> Decoder<R> {
self
}
pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> {
Ok((self.image.width, self.image.height))
}
pub fn colortype(&mut self) -> TiffResult<ColorType> {
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<IfdPointer> {
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<R: Read + Seek> Decoder<R> {
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<Directory> {
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<R: Read + Seek> Decoder<R> {
pub fn byte_order(&self) -> ByteOrder {
self.value_reader.reader.byte_order
}
}
/// <span id=header-stream-access>Methods that interact with the underlying reader.</span>
impl<R: Read> Decoder<R> {
/// 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<u64, io::Error> {
@@ -859,11 +929,6 @@ impl<R: Read + Seek> Decoder<R> {
}
}
/// 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<u8, io::Error> {
@@ -953,38 +1018,24 @@ impl<R: Read + Seek> Decoder<R> {
/// 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<Directory> {
self.value_reader.read_directory(ptr)
}
/// <span id=header-image>Methods that interact with the current directory as an image.</span>
impl<R: Read + Seek> Decoder<R> {
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<R: Read + Seek> Decoder<R> {
Ok(())
}
pub fn dimensions(&mut self) -> TiffResult<(u32, u32)> {
Ok((self.image.width, self.image.height))
}
pub fn colortype(&mut self) -> TiffResult<ColorType> {
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<R: Read + Seek> Decoder<R> {
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<R: Read + Seek> Decoder<R> {
/// Tries to retrieve a tag from the current image directory.
+5 -6
View File
@@ -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
)
+6 -8
View File
@@ -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"),
+8 -4
View File
@@ -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()
+40 -11
View File
@@ -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::<Vec<_>>();
@@ -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::<Vec<_>>();
@@ -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<R: std::io::Read + std::io::Seek> AssertDecode for Decoder<R> {
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<u32> {
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<i32> {
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<u64> {
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<i64> {
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()