From 170bd3697c4d7f04769c82565c1c40cd99f1ad76 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 2 Nov 2025 14:54:15 +0100 Subject: [PATCH 1/5] Provide tags in their byte encoding --- src/tags.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/tags.rs b/src/tags.rs index 8dc8716..ae10ed4 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -336,6 +336,62 @@ pub enum ExtraSamples(u16) { } } +/// A value represented as in-memory bytes with flexible byteorder. +pub struct ValueBuffer { + /// The raw bytes of the value. + bytes: Vec, + + /// The type of the value. + ty: Type, + + /// The byte order of the value. + byte_order: ByteOrder, +} + +impl ValueBuffer { + /// A value with a count of zero. + /// + /// The byte order is set to the native byte order of the platform. + pub fn empty(ty: Type) -> Self { + ValueBuffer { + bytes: vec![], + ty, + byte_order: ByteOrder::native(), + } + } + + pub fn byte_order(&self) -> ByteOrder { + self.byte_order + } + + pub fn tiff_type(&self) -> Type { + self.ty + } + + /// The count of items in the value. + pub fn count(&self) -> u64 { + (self.bytes.len() / usize::from(self.ty.byte_len())) as u64 + } + + /// View the underlying raw bytes of this value. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// View the underlying mutable raw bytes of this value. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + &mut self.bytes + } + + /// Change the byte order of the value representation. + pub fn set_byte_order(&mut self, byte_order: ByteOrder) { + self.byte_order + .convert(self.ty, self.bytes.as_mut_slice(), byte_order); + + self.byte_order = byte_order; + } +} + /// Byte order of the TIFF file. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ByteOrder { From 45f617b9f5383bbb0cbf2e219ae293ca33f66ea2 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:40:24 +0100 Subject: [PATCH 2/5] Construct tags::ValueBuffer from ifd::Value Provides interaction between decoder and encoder values. Not sure about this commit yet, the implementation is a bit odd. --- Cargo.toml | 1 + src/tags.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 82118fe..45a9a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ categories = ["multimedia::images", "multimedia::encoding"] exclude = ["tests/images/*", "tests/fuzz_images/*"] [dependencies] +bytemuck = "1.14" half = { version = "2.4.1" } quick-error = "2.0.1" diff --git a/src/tags.rs b/src/tags.rs index ae10ed4..7a9aaf9 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,3 +1,5 @@ +use crate::decoder::ifd; + macro_rules! tags { { // Permit arbitrary meta items, which include documentation. @@ -392,6 +394,111 @@ impl ValueBuffer { } } +/// Create a value with native endian byte order. +impl TryFrom for ValueBuffer { + type Error = crate::TiffError; + + fn try_from(value: ifd::Value) -> Result { + Self::try_from(&value) + } +} + +/// Create a value with native endian byte order. +impl TryFrom<&'_ ifd::Value> for ValueBuffer { + type Error = crate::TiffError; + + fn try_from(value: &'_ ifd::Value) -> Result { + use std::borrow::Cow::{self, Borrowed, Owned}; + + fn bytes_of_value( + value: &'_ ifd::Value, + ) -> Result<(Cow<'_, [u8]>, Type), crate::TiffError> { + Ok(match value { + ifd::Value::List(values) => { + let mut type_ = None; + let mut bytes = vec![]; + + // This is treated separately, also we rely on the caller being faithful in their + // use of the ifd::Value enum. + for item in values { + let (value, newty) = bytes_of_value(item)?; + + if type_.map_or(false, |ty| ty != newty) { + return Err(crate::TiffError::UnsupportedError( + crate::error::TiffUnsupportedError::UnsupportedDataType, + )); + } + + type_ = Some(newty); + bytes.extend_from_slice(&value); + } + + let Some(ty) = type_ else { + return Err(crate::TiffError::UnsupportedError( + crate::error::TiffUnsupportedError::UnsupportedDataType, + )); + }; + + (Owned(bytes), ty) + } + ifd::Value::Byte(val) => (Borrowed(bytemuck::bytes_of(val)), Type::BYTE), + ifd::Value::Short(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SHORT), + ifd::Value::SignedByte(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SBYTE), + ifd::Value::SignedShort(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SSHORT), + ifd::Value::Signed(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SLONG), + ifd::Value::SignedBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SLONG8), + ifd::Value::Unsigned(val) => (Borrowed(bytemuck::bytes_of(val)), Type::LONG), + ifd::Value::UnsignedBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::LONG8), + ifd::Value::Float(val) => (Borrowed(bytemuck::bytes_of(val)), Type::FLOAT), + ifd::Value::Double(val) => (Borrowed(bytemuck::bytes_of(val)), Type::DOUBLE), + ifd::Value::Rational(num, denom) => ( + Owned([bytemuck::bytes_of(num), bytemuck::bytes_of(denom)].concat()), + Type::RATIONAL, + ), + #[expect(deprecated)] + ifd::Value::RationalBig(..) | ifd::Value::SRationalBig(..) => { + return Err(crate::TiffError::UnsupportedError( + crate::error::TiffUnsupportedError::UnsupportedDataType, + )); + } + ifd::Value::SRational(num, denom) => ( + Owned([bytemuck::bytes_of(num), bytemuck::bytes_of(denom)].concat()), + Type::SRATIONAL, + ), + ifd::Value::Ascii(st) => (Borrowed(st.as_bytes()), Type::ASCII), + ifd::Value::Ifd(val) => (Borrowed(bytemuck::bytes_of(val)), Type::IFD), + ifd::Value::IfdBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::IFD8), + }) + } + + let byte_order = ByteOrder::native(); + + let (bytes, ty) = bytes_of_value(value)?; + let bytes = bytes.into_owned(); + + // Check the count can be represented. + if let ifd::Value::List(values) = value { + let count = + u64::try_from(values.len()).map_err(|_| crate::TiffError::LimitsExceeded)?; + let expected = ty.value_bytes(count)?; + + if bytes.len() != expected as usize { + return Err(crate::TiffError::UnsupportedError( + crate::TiffUnsupportedError::UnsupportedDataType, + )); + } + } else { + debug_assert_eq!(bytes.len(), ty.byte_len().into()); + } + + Ok(ValueBuffer { + bytes, + byte_order, + ty, + }) + } +} + /// Byte order of the TIFF file. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ByteOrder { From 1dab89f10eef5978c0468f88624b57a75be6940f Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:35:55 +0100 Subject: [PATCH 3/5] Generalize write for pre-encoded tags --- src/encoder/mod.rs | 108 ++++++++++++++++++++++++++++++++++---- src/encoder/tiff_value.rs | 5 ++ src/error.rs | 3 ++ src/tags.rs | 2 +- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 93256d1..c5c8e72 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -12,8 +12,8 @@ use crate::{ decoder::ifd::Entry, error::{TiffResult, UsageError}, tags::{ - CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit, - SampleFormat, Tag, Type, + ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, + ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer, }, Directory, TiffError, TiffFormatError, }; @@ -291,6 +291,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { }) } + /// Write a tag-value pair with prepared byte data. + /// + /// Note that the library will _not_ attempt to verify that the data type or the count of the + /// opaque value is permissible for the given tag. The data will be associated with the tag + /// exactly as-is. + /// + /// An error is returned if the byte order of the presented value does not match the byte order + /// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)). + pub fn write_tag_value(&mut self, tag: Tag, value: &ValueBuffer) -> TiffResult<()> { + let entry = self.write_entry_buf(value)?; + self.directory.extend([(tag, entry)]); + + Ok(()) + } + /// Write a single ifd tag. pub fn write_tag(&mut self, tag: Tag, value: T) -> TiffResult<()> { // Encodes the value if necessary. In the current bytes all integers are taken as native @@ -301,12 +316,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { value.write(&mut writer)?; } - let entry = Self::write_value( + let entry = Self::write_entry_inner( self.writer, - &DirectoryEntry { + DirectoryEntry { data_type: ::FIELD_TYPE, count: value.count().try_into()?, - data: bytes, + data: bytes.into(), }, )?; @@ -317,13 +332,70 @@ 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. + /// This could be used to write tiff strips. All of the data will be written into the file as a + /// slice of bytes. It can not be used as an entry in a directory directly, where very short + /// values are instead represented in the offset field directly. Use [`Self::write_entry`] + /// instead. pub fn write_data(&mut self, value: T) -> TiffResult { let offset = self.writer.offset(); value.write(self.writer)?; Ok(offset) } + /// Write some data directly to the tiff file, the offset of the data is returned. + /// + /// All of the data will be written into the file as a slice of bytes. It can not be used as an + /// entry in a directory directly, where very short values are instead represented in the + /// offset field directly. Use [`Self::write_entry_buf`] instead. + /// + /// An error is returned if the byte order of the presented value does not match the byte order + /// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)). + pub fn write_data_buf(&mut self, value: &ValueBuffer) -> TiffResult { + self.check_value_byteorder(value)?; + let offset = self.writer.offset(); + self.writer.write_bytes(value.as_bytes())?; + Ok(offset) + } + + /// Write a directory value into the file. + /// + /// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but + /// stored in the entry's offset field. + pub fn write_entry(&mut self, value: T) -> TiffResult { + Self::write_entry_inner( + self.writer, + DirectoryEntry { + data_type: T::FIELD_TYPE, + count: K::convert_offset(value.count() as u64)?, + // FIXME: not optimal we always allocate here. But the only other method we have + // available is `T::write(&mut TiffWriter)` and we must pass that into + // `write_entry_inner` but then have a combinatorial explosion of instantiations + // which is absurd. + data: value.data(), + }, + ) + } + + /// Write a directory value into the file. + /// + /// An error is returned if the byte order of the presented value does not match the byte order + /// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)). + /// + /// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but + /// stored in the entry's offset field. + pub fn write_entry_buf(&mut self, value: &ValueBuffer) -> TiffResult { + self.check_value_byteorder(value)?; + + Self::write_entry_inner( + self.writer, + DirectoryEntry { + data_type: value.data_type(), + count: K::convert_offset(value.count())?, + data: value.as_bytes().into(), + }, + ) + } + /// Insert previously written key-value entries. /// /// It is the caller's responsibility to ensure that the data referred to by the entries is @@ -377,17 +449,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { Ok(offset) } - fn write_value( + // Does it really make sense that this would be a (potentially) compressible write? For actual + // directory values their size must match the data written so there's going to be a silent + // problem if that actually occurs. + fn write_entry_inner( writer: &mut TiffWriter, - value: &DirectoryEntry, + value: DirectoryEntry, ) -> TiffResult { - let &DirectoryEntry { + let DirectoryEntry { data: ref bytes, ref count, data_type, } = value; let in_entry_bytes = mem::size_of::(); + let mut offset_bytes = [0; 8]; if bytes.len() > in_entry_bytes { @@ -406,6 +482,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { // and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it. // Instead, write it to a buffer... let mut count_bytes = [0; 8]; + debug_assert!(in_entry_bytes == 4 || in_entry_bytes == 8); // Nominally Cow but we only expect `Cow::Borrowed`. count_bytes[..count.bytes()].copy_from_slice(&count.data()); @@ -419,6 +496,15 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { }) } + fn check_value_byteorder(&self, value: &ValueBuffer) -> TiffResult<()> { + // FIXME: we should enable writing files with any chosen byte order. + if value.byte_order() != ByteOrder::native() { + Err(TiffError::UsageError(UsageError::ByteOrderMismatch)) + } else { + Ok(()) + } + } + /// Provides the number of bytes written by the underlying TiffWriter during the last call. fn last_written(&self) -> u64 { self.writer.last_written() @@ -824,10 +910,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W } } -struct DirectoryEntry { +struct DirectoryEntry<'data, S> { data_type: Type, count: S, - data: Vec, + data: std::borrow::Cow<'data, [u8]>, } /// Trait to abstract over Tiff/BigTiff differences. diff --git a/src/encoder/tiff_value.rs b/src/encoder/tiff_value.rs index b40d0f4..38eeac3 100644 --- a/src/encoder/tiff_value.rs +++ b/src/encoder/tiff_value.rs @@ -16,7 +16,12 @@ use super::writer::TiffWriter; pub trait TiffValue { const BYTE_LEN: u8; const FIELD_TYPE: Type; + + // FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or + // `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX` + // for classic tiff. fn count(&self) -> usize; + fn bytes(&self) -> usize { self.count() * usize::from(Self::BYTE_LEN) } diff --git a/src/error.rs b/src/error.rs index eb4fc86..94dc468 100644 --- a/src/error.rs +++ b/src/error.rs @@ -208,6 +208,9 @@ quick_error! { ReconfiguredAfterImageWrite { display("attempted to reconfigure the encoder after image writing has started") } + ByteOrderMismatch { + display("attempted to use data with a byte order that does not match the required one") + } } } diff --git a/src/tags.rs b/src/tags.rs index 7a9aaf9..fe0c697 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -366,7 +366,7 @@ impl ValueBuffer { self.byte_order } - pub fn tiff_type(&self) -> Type { + pub fn data_type(&self) -> Type { self.ty } From 74b285736424c33b45976339f747e2de93648d0a Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:24:48 +0100 Subject: [PATCH 4/5] Construct ValueBuffer from TiffValue --- src/encoder/mod.rs | 2 +- src/tags.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index c5c8e72..e67d76b 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -294,7 +294,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> { /// Write a tag-value pair with prepared byte data. /// /// Note that the library will _not_ attempt to verify that the data type or the count of the - /// opaque value is permissible for the given tag. The data will be associated with the tag + /// buffered value is permissible for the given tag. The data will be associated with the tag /// exactly as-is. /// /// An error is returned if the byte order of the presented value does not match the byte order diff --git a/src/tags.rs b/src/tags.rs index fe0c697..aa82e3e 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,4 +1,4 @@ -use crate::decoder::ifd; +use crate::{decoder::ifd, encoder::TiffValue}; macro_rules! tags { { @@ -362,6 +362,15 @@ impl ValueBuffer { } } + /// Create a value with native byte order from in-memory data. + pub fn from_value(value: &T) -> Self { + ValueBuffer { + bytes: value.data().into_owned(), + ty: ::FIELD_TYPE, + byte_order: ByteOrder::native(), + } + } + pub fn byte_order(&self) -> ByteOrder { self.byte_order } From 915cc20945747eb04709e0ea7ac19920d22e0805 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:46:27 +0100 Subject: [PATCH 5/5] Revert "Construct tags::ValueBuffer from ifd::Value" This reverts commit 45f617b9f5383bbb0cbf2e219ae293ca33f66ea2 --- Cargo.toml | 1 - src/tags.rs | 107 +--------------------------------------------------- 2 files changed, 1 insertion(+), 107 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45a9a23..82118fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ categories = ["multimedia::images", "multimedia::encoding"] exclude = ["tests/images/*", "tests/fuzz_images/*"] [dependencies] -bytemuck = "1.14" half = { version = "2.4.1" } quick-error = "2.0.1" diff --git a/src/tags.rs b/src/tags.rs index aa82e3e..e78dde5 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,4 +1,4 @@ -use crate::{decoder::ifd, encoder::TiffValue}; +use crate::encoder::TiffValue; macro_rules! tags { { @@ -403,111 +403,6 @@ impl ValueBuffer { } } -/// Create a value with native endian byte order. -impl TryFrom for ValueBuffer { - type Error = crate::TiffError; - - fn try_from(value: ifd::Value) -> Result { - Self::try_from(&value) - } -} - -/// Create a value with native endian byte order. -impl TryFrom<&'_ ifd::Value> for ValueBuffer { - type Error = crate::TiffError; - - fn try_from(value: &'_ ifd::Value) -> Result { - use std::borrow::Cow::{self, Borrowed, Owned}; - - fn bytes_of_value( - value: &'_ ifd::Value, - ) -> Result<(Cow<'_, [u8]>, Type), crate::TiffError> { - Ok(match value { - ifd::Value::List(values) => { - let mut type_ = None; - let mut bytes = vec![]; - - // This is treated separately, also we rely on the caller being faithful in their - // use of the ifd::Value enum. - for item in values { - let (value, newty) = bytes_of_value(item)?; - - if type_.map_or(false, |ty| ty != newty) { - return Err(crate::TiffError::UnsupportedError( - crate::error::TiffUnsupportedError::UnsupportedDataType, - )); - } - - type_ = Some(newty); - bytes.extend_from_slice(&value); - } - - let Some(ty) = type_ else { - return Err(crate::TiffError::UnsupportedError( - crate::error::TiffUnsupportedError::UnsupportedDataType, - )); - }; - - (Owned(bytes), ty) - } - ifd::Value::Byte(val) => (Borrowed(bytemuck::bytes_of(val)), Type::BYTE), - ifd::Value::Short(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SHORT), - ifd::Value::SignedByte(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SBYTE), - ifd::Value::SignedShort(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SSHORT), - ifd::Value::Signed(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SLONG), - ifd::Value::SignedBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::SLONG8), - ifd::Value::Unsigned(val) => (Borrowed(bytemuck::bytes_of(val)), Type::LONG), - ifd::Value::UnsignedBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::LONG8), - ifd::Value::Float(val) => (Borrowed(bytemuck::bytes_of(val)), Type::FLOAT), - ifd::Value::Double(val) => (Borrowed(bytemuck::bytes_of(val)), Type::DOUBLE), - ifd::Value::Rational(num, denom) => ( - Owned([bytemuck::bytes_of(num), bytemuck::bytes_of(denom)].concat()), - Type::RATIONAL, - ), - #[expect(deprecated)] - ifd::Value::RationalBig(..) | ifd::Value::SRationalBig(..) => { - return Err(crate::TiffError::UnsupportedError( - crate::error::TiffUnsupportedError::UnsupportedDataType, - )); - } - ifd::Value::SRational(num, denom) => ( - Owned([bytemuck::bytes_of(num), bytemuck::bytes_of(denom)].concat()), - Type::SRATIONAL, - ), - ifd::Value::Ascii(st) => (Borrowed(st.as_bytes()), Type::ASCII), - ifd::Value::Ifd(val) => (Borrowed(bytemuck::bytes_of(val)), Type::IFD), - ifd::Value::IfdBig(val) => (Borrowed(bytemuck::bytes_of(val)), Type::IFD8), - }) - } - - let byte_order = ByteOrder::native(); - - let (bytes, ty) = bytes_of_value(value)?; - let bytes = bytes.into_owned(); - - // Check the count can be represented. - if let ifd::Value::List(values) = value { - let count = - u64::try_from(values.len()).map_err(|_| crate::TiffError::LimitsExceeded)?; - let expected = ty.value_bytes(count)?; - - if bytes.len() != expected as usize { - return Err(crate::TiffError::UnsupportedError( - crate::TiffUnsupportedError::UnsupportedDataType, - )); - } - } else { - debug_assert_eq!(bytes.len(), ty.byte_len().into()); - } - - Ok(ValueBuffer { - bytes, - byte_order, - ty, - }) - } -} - /// Byte order of the TIFF file. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ByteOrder {