From 7b1683687ed3912601fa2099bb4b8c768e28f0e7 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:59:53 +0200 Subject: [PATCH 1/2] Split Encoder::write_directory There are two kinds of directories: those that are image directories directly in the TIFF file's main sequence, and directories that follow the structure but represent auxiliary data. The method will link the directory into the main sequence but those images have additional requirement on their IFDs which is not clear to the caller. We make that explicit and also provide a way to write auxiliary directories. --- src/encoder/mod.rs | 95 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index a6661c3..449ca76 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ decoder::ifd::Entry, error::{TiffResult, UsageError}, - tags::{CompressionMethod, ResolutionUnit, SampleFormat, Tag, Type}, + tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type}, Directory, TiffError, TiffFormatError, }; @@ -170,10 +170,29 @@ impl TiffEncoder { } /// Create a [`DirectoryEncoder`] to encode an ifd directory. + #[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"] + #[doc(hidden)] pub fn new_directory(&mut self) -> TiffResult> { Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain) } + /// Create a [`DirectoryEncoder`] to encode an ifd directory. + /// + /// The caller is responsible for ensuring that the directory is a valid image in the main TIFF + /// IFD sequence. To encode additional directories that are not linked into the sequence, use + /// [`Self::extra_directory`][TiffEncoder::extra_directory]. + pub fn image_directory(&mut self) -> TiffResult> { + Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain) + } + + /// Create a [`DirectoryEncoder`] to encode an ifd directory. + /// + /// The directory is not linked into the sequence of directories. For instance, encode Exif + /// directories or SubIfd directories with this method. + pub fn extra_directory(&mut self) -> TiffResult> { + Self::unchained_directory(&mut self.writer) + } + /// Create an [`ImageEncoder`] to encode an image one slice at a time. pub fn new_image( &mut self, @@ -207,6 +226,10 @@ impl TiffEncoder { let last_ifd = *last_ifd_chain; DirectoryEncoder::new(writer, Some(last_ifd), Some(last_ifd_chain)) } + + fn unchained_directory(writer: &mut TiffWriter) -> TiffResult> { + DirectoryEncoder::new(writer, None, None) + } } /// Low level interface to encode ifd directories. @@ -225,6 +248,15 @@ pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> { dropped: bool, } +/// The offset of an encoded directory in the file. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct DirectoryOffset { + /// The start of the directory, as a Tiff value. + pub offset: IfdPointer, + /// The offset of its sequence link field, in our private representation. + ifd_chain: NonZeroU64, +} + impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { /// Construct a directory writer appending to data, assuming the writer is currently positioned /// immediately after a previously written IFD to which to append. @@ -268,6 +300,35 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { Ok(()) } + /// Write some data to the tiff file, the offset of the data is returned. + /// + /// This could be used to write tiff strips. + pub fn write_data(&mut self, value: T) -> TiffResult { + let offset = self.writer.offset(); + value.write(self.writer)?; + Ok(offset) + } + + /// Define the parent directory. + /// + /// Each directory has an offset based link to its successor, forming a linked list in the + /// file. The encoder writes its own offset into the parent's field when [`Self::finish`] is + /// called or it is dropped. This redefines the parent. An offset representation of the parent + /// must be acquired by finishing it with [`Self::finish_with_offsets`]. + pub fn set_parent(&mut self, offset: &DirectoryOffset) { + self.chained_ifd_pos = Some(offset.ifd_chain); + } + + /// Write out the ifd directory itself. + pub fn finish(mut self) -> TiffResult<()> { + self.finish_internal()?; + Ok(()) + } + + pub fn finish_with_offsets(mut self) -> TiffResult { + self.finish_internal() + } + fn write_directory(&mut self) -> TiffResult { // Start by turning all buffered unwritten values into entries. let offset = self.writer.offset(); @@ -327,21 +388,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { }) } - /// Write some data to the tiff file, the offset of the data is returned. - /// - /// This could be used to write tiff strips. - pub fn write_data(&mut self, value: T) -> TiffResult { - let offset = self.writer.offset(); - value.write(self.writer)?; - Ok(offset) - } - /// Provides the number of bytes written by the underlying TiffWriter during the last call. fn last_written(&self) -> u64 { self.writer.last_written() } - fn finish_internal(&mut self) -> TiffResult<()> { + fn finish_internal(&mut self) -> TiffResult { let ifd_pointer = self.write_directory()?; if let Some(prior) = self.chained_ifd_pos { @@ -355,19 +407,19 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { K::write_offset(self.writer, 0)?; + let ifd_chain = NonZeroU64::new(self.writer.previous_ifd_pointer::()) + .expect("IFD chain field is at a non-zero offset"); + if let Some(prior) = self.write_chain.take() { - *prior = NonZeroU64::new(self.writer.previous_ifd_pointer::()) - .expect("IFD chain field is at a non-zero offset"); + *prior = ifd_chain; } self.dropped = true; - Ok(()) - } - - /// Write out the ifd directory. - pub fn finish(mut self) -> TiffResult<()> { - self.finish_internal() + Ok(DirectoryOffset { + offset: IfdPointer(ifd_pointer), + ifd_chain, + }) } } @@ -643,7 +695,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, Ok(()) } - fn finish_internal(&mut self) -> TiffResult<()> { + fn finish_internal(&mut self) -> TiffResult { self.encoder .write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?; self.encoder.write_tag( @@ -662,7 +714,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, /// Write out image and ifd directory. pub fn finish(mut self) -> TiffResult<()> { - self.finish_internal() + self.finish_internal()?; + Ok(()) } } From ab4fb4e959927cd4fc2d559c20441751282881bb Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:28:24 +0200 Subject: [PATCH 2/2] Test and polish encoding extra directories --- src/encoder/mod.rs | 32 ++++++++++++++++++------ tests/encode_images.rs | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 449ca76..1c95821 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -250,11 +250,20 @@ pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> { /// The offset of an encoded directory in the file. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct DirectoryOffset { - /// The start of the directory, as a Tiff value. - pub offset: IfdPointer, +pub struct DirectoryOffset { + /// The start of the directory as a Tiff value. + /// + /// This is a bit of a wart in the strongly typed design of the encoder. The value _type_ must + /// itself know how to be represented in the Tiff file but that may differ based on endianess + /// as well as the offset size (`u32` or BigTIFF's `u64`). Thankfully we're allowed to + /// represent offsets with `LONG` or `IFD` in the usual case. + pub offset: K::OffsetType, + /// The start of the directory as a pure offset. + pub pointer: IfdPointer, /// The offset of its sequence link field, in our private representation. ifd_chain: NonZeroU64, + /// The kind of Tiff file the offset is for. + kind: PhantomData, } impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { @@ -315,7 +324,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { /// file. The encoder writes its own offset into the parent's field when [`Self::finish`] is /// called or it is dropped. This redefines the parent. An offset representation of the parent /// must be acquired by finishing it with [`Self::finish_with_offsets`]. - pub fn set_parent(&mut self, offset: &DirectoryOffset) { + pub fn set_parent(&mut self, offset: &DirectoryOffset) { self.chained_ifd_pos = Some(offset.ifd_chain); } @@ -325,7 +334,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { Ok(()) } - pub fn finish_with_offsets(mut self) -> TiffResult { + pub fn finish_with_offsets(mut self) -> TiffResult> { self.finish_internal() } @@ -393,13 +402,18 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { self.writer.last_written() } - fn finish_internal(&mut self) -> TiffResult { + fn finish_internal(&mut self) -> TiffResult> { let ifd_pointer = self.write_directory()?; + let offset = K::convert_offset(ifd_pointer)?; if let Some(prior) = self.chained_ifd_pos { let curr_pos = self.writer.offset(); self.writer.goto_offset(prior.get())?; + // Note how we are not writing the `offset` type itself here as doing so would need to + // go through the `TiffValue` trait—the type is not constrained by much. But for the + // trait we need a `TiffWriter` which comes with a bunch of additional state such as + // compressor etc. that we have no need for. K::write_offset(self.writer, ifd_pointer)?; self.writer.goto_offset(curr_pos)?; @@ -417,8 +431,10 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { self.dropped = true; Ok(DirectoryOffset { - offset: IfdPointer(ifd_pointer), + pointer: IfdPointer(ifd_pointer), + offset, ifd_chain, + kind: PhantomData, }) } } @@ -695,7 +711,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, Ok(()) } - fn finish_internal(&mut self) -> TiffResult { + fn finish_internal(&mut self) -> TiffResult> { self.encoder .write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?; self.encoder.write_tag( diff --git a/tests/encode_images.rs b/tests/encode_images.rs index fb71ca5..af9f610 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -626,3 +626,59 @@ fn test_rows_per_strip() { } } } + +#[test] +fn test_auxiliary_directory() { + const ARTIST: &str = "Auxiliary-tiff"; + + let mut data = Cursor::new(Vec::new()); + let mut tiff = TiffEncoder::new(&mut data).unwrap(); + + { + let mut image_encoder = tiff.new_image::(1, 1).unwrap(); + image_encoder.write_strip(&[1]).unwrap(); + } + + let exif = { + let mut extra = tiff.extra_directory().unwrap(); + extra.write_tag(Tag::Artist, ARTIST).unwrap(); + extra.finish_with_offsets().unwrap() + }; + + { + let mut image_encoder = tiff.new_image::(2, 1).unwrap(); + image_encoder.write_strip(&[1, 2]).unwrap(); + let encoder = image_encoder.encoder(); + encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap(); + } + + drop(tiff); + + data.set_position(0); + let mut decoder = Decoder::new(&mut data).unwrap(); + + { + assert_eq!(decoder.dimensions().unwrap(), (1, 1)); + let _ = decoder.read_image().unwrap(); + } + + { + decoder.next_image().unwrap(); + assert_eq!(decoder.dimensions().unwrap(), (2, 1)); + let _ = decoder.read_image().unwrap(); + + let exif_location = decoder + .get_tag(Tag::ExifDirectory) + .expect("second directory missing Exif") + .into_ifd_pointer() + .expect("exif directory is not an IFD pointer"); + + let exif = decoder.read_directory(exif_location).unwrap(); + + let artist = decoder + .read_directory_tags(&exif) + .get_tag_ascii_string(Tag::Artist) + .expect("EXIF directory missing artist tag"); + assert_eq!(artist, ARTIST); + } +}