Repository: image-rs/image-tiff
Pull request #283 (2025-07-13 21:32:41)

Write extra directories
Expands the encoder such that we can write auxiliary directories, and chain them manually without interfering with the main image sequence. End-to-end tested with our decoder's new capabilities of reading such auxiliary directories and tags.

Deprecates the `new_directory` method on the encoder. The name does not invoke sufficient intent. The directory _is_ chained into the image sequence but no documentation or name required the caller to uphold the invariants that the TIFF standard requires from such IFDs, i.e. that they actually contain images.

Instead, two methods are now available:
- `image_directory` which has the old behavior but with explicit intent.
- `extra_directory` which clarifies that it will not be linked.


Patch:
diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs
index a6661c3..1c95821 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<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
     }
 
     /// 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<DirectoryEncoder<'_, W, K>> {
         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<DirectoryEncoder<'_, W, K>> {
+        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<DirectoryEncoder<'_, W, K>> {
+        Self::unchained_directory(&mut self.writer)
+    }
+
     /// Create an [`ImageEncoder`] to encode an image one slice at a time.
     pub fn new_image<C: ColorType>(
         &mut self,
@@ -207,6 +226,10 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
         let last_ifd = *last_ifd_chain;
         DirectoryEncoder::new(writer, Some(last_ifd), Some(last_ifd_chain))
     }
+
+    fn unchained_directory(writer: &mut TiffWriter<W>) -> TiffResult<DirectoryEncoder<'_, W, K>> {
+        DirectoryEncoder::new(writer, None, None)
+    }
 }
 
 /// Low level interface to encode ifd directories.
@@ -225,6 +248,24 @@ 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<K: TiffKind> {
+    /// 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<K>,
+}
+
 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 +309,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<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
+        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<K>) {
+        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<DirectoryOffset<K>> {
+        self.finish_internal()
+    }
+
     fn write_directory(&mut self) -> TiffResult<u64> {
         // Start by turning all buffered unwritten values into entries.
         let offset = self.writer.offset();
@@ -327,27 +397,23 @@ 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<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
-        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<DirectoryOffset<K>> {
         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)?;
@@ -355,19 +421,21 @@ 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::<K>())
+            .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::<K>())
-                .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 {
+            pointer: IfdPointer(ifd_pointer),
+            offset,
+            ifd_chain,
+            kind: PhantomData,
+        })
     }
 }
 
@@ -643,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<DirectoryOffset<K>> {
         self.encoder
             .write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
         self.encoder.write_tag(
@@ -662,7 +730,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(())
     }
 }
 


Test patch:
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::<colortype::Gray8>(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::<colortype::Gray8>(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);
+    }
+}