From c746653269cb9173601da77561603a1a7bb58373 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:49:31 +0200 Subject: [PATCH 1/7] Add Decoder::read_image_bytes, borrowed output --- src/decoder/mod.rs | 144 ++++++++++++++++++++++++++++++++++++--------- src/error.rs | 3 + 2 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 0f91d23..4576868 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,3 +1,4 @@ +use std::alloc::{Layout, LayoutError}; use std::collections::BTreeMap; use std::io::{self, Read, Seek}; @@ -197,6 +198,61 @@ impl<'a> DecodingBuffer<'a> { } } +/// The count and matching discriminant for a `DecodingBuffer`. +#[derive(Clone)] +enum DecodingExtent { + U8(usize), + U16(usize), + U32(usize), + U64(usize), + F16(usize), + F32(usize), + F64(usize), + I8(usize), + I16(usize), + I32(usize), + I64(usize), +} + +impl DecodingExtent { + fn to_result_buffer(&self, limits: &Limits) -> TiffResult { + match *self { + DecodingExtent::U8(count) => DecodingResult::new_u8(count, limits), + DecodingExtent::U16(count) => DecodingResult::new_u16(count, limits), + DecodingExtent::U32(count) => DecodingResult::new_u32(count, limits), + DecodingExtent::U64(count) => DecodingResult::new_u64(count, limits), + DecodingExtent::F16(count) => DecodingResult::new_f16(count, limits), + DecodingExtent::F32(count) => DecodingResult::new_f32(count, limits), + DecodingExtent::F64(count) => DecodingResult::new_f64(count, limits), + DecodingExtent::I8(count) => DecodingResult::new_i8(count, limits), + DecodingExtent::I16(count) => DecodingResult::new_i16(count, limits), + DecodingExtent::I32(count) => DecodingResult::new_i32(count, limits), + DecodingExtent::I64(count) => DecodingResult::new_i64(count, limits), + } + } + + fn preferred_layout(self) -> TiffResult { + fn overflow(_: LayoutError) -> TiffError { + TiffError::LimitsExceeded + } + + match self { + DecodingExtent::U8(count) => Layout::array::(count), + DecodingExtent::U16(count) => Layout::array::(count), + DecodingExtent::U32(count) => Layout::array::(count), + DecodingExtent::U64(count) => Layout::array::(count), + DecodingExtent::F16(count) => Layout::array::(count), + DecodingExtent::F32(count) => Layout::array::(count), + DecodingExtent::F64(count) => Layout::array::(count), + DecodingExtent::I8(count) => Layout::array::(count), + DecodingExtent::I16(count) => Layout::array::(count), + DecodingExtent::I32(count) => Layout::array::(count), + DecodingExtent::I64(count) => Layout::array::(count), + } + .map_err(overflow) + } +} + #[derive(Debug, Copy, Clone, PartialEq)] /// Chunk type of the internal representation pub enum ChunkType { @@ -856,6 +912,11 @@ impl Decoder { } fn result_buffer(&self, width: usize, height: usize) -> TiffResult { + self.result_extent(width, height)? + .to_result_buffer(&self.value_reader.limits) + } + + fn result_extent(&self, width: usize, height: usize) -> TiffResult { let bits_per_sample = self.image().bits_per_sample; let row_samples = if bits_per_sample >= 8 { @@ -872,37 +933,44 @@ impl Decoder { .ok_or(TiffError::LimitsExceeded)?; let max_sample_bits = self.image().bits_per_sample; - let limits = &self.value_reader.limits; - match self.image().sample_format { + Ok(match self.image().sample_format { SampleFormat::Uint => match max_sample_bits { - n if n <= 8 => DecodingResult::new_u8(buffer_size, limits), - n if n <= 16 => DecodingResult::new_u16(buffer_size, limits), - n if n <= 32 => DecodingResult::new_u32(buffer_size, limits), - n if n <= 64 => DecodingResult::new_u64(buffer_size, limits), - n => Err(TiffError::UnsupportedError( - TiffUnsupportedError::UnsupportedBitsPerChannel(n), - )), + n if n <= 8 => DecodingExtent::U8(buffer_size), + n if n <= 16 => DecodingExtent::U16(buffer_size), + n if n <= 32 => DecodingExtent::U32(buffer_size), + n if n <= 64 => DecodingExtent::U64(buffer_size), + n => { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::UnsupportedBitsPerChannel(n), + )) + } }, SampleFormat::IEEEFP => match max_sample_bits { - 16 => DecodingResult::new_f16(buffer_size, limits), - 32 => DecodingResult::new_f32(buffer_size, limits), - 64 => DecodingResult::new_f64(buffer_size, limits), - n => Err(TiffError::UnsupportedError( - TiffUnsupportedError::UnsupportedBitsPerChannel(n), - )), + 16 => DecodingExtent::F16(buffer_size), + 32 => DecodingExtent::F32(buffer_size), + 64 => DecodingExtent::F64(buffer_size), + n => { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::UnsupportedBitsPerChannel(n), + )) + } }, SampleFormat::Int => match max_sample_bits { - n if n <= 8 => DecodingResult::new_i8(buffer_size, limits), - n if n <= 16 => DecodingResult::new_i16(buffer_size, limits), - n if n <= 32 => DecodingResult::new_i32(buffer_size, limits), - n if n <= 64 => DecodingResult::new_i64(buffer_size, limits), - n => Err(TiffError::UnsupportedError( - TiffUnsupportedError::UnsupportedBitsPerChannel(n), - )), + n if n <= 8 => DecodingExtent::I8(buffer_size), + n if n <= 16 => DecodingExtent::I16(buffer_size), + n if n <= 32 => DecodingExtent::I32(buffer_size), + n if n <= 64 => DecodingExtent::I64(buffer_size), + n => { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::UnsupportedBitsPerChannel(n), + )) + } }, - format => Err(TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into()), - } + format => { + return Err(TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into()) + } + }) } /// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector. @@ -934,9 +1002,31 @@ impl Decoder { pub fn read_image(&mut self) -> TiffResult { let width = self.image().width; let height = self.image().height; + let mut result = self.result_buffer(width as usize, height as usize)?; + self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?; + + Ok(result) + } + + pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { + let width = self.image().width; + let height = self.image().height; + + let extent = self.result_extent(width as usize, height as usize)?; + let needed_bytes = extent.preferred_layout()?.size(); + + if buffer.len() < needed_bytes { + return Err(TiffError::UsageError( + UsageError::InsufficientOutputBufferSize { + needed: needed_bytes, + provided: buffer.len(), + }, + )); + } + if width == 0 || height == 0 { - return Ok(result); + return Ok(()); } let chunk_dimensions = self.image().chunk_dimensions()?; @@ -989,13 +1079,13 @@ impl Decoder { y * output_row_stride * chunk_dimensions.1 as usize + x * chunk_row_bytes; self.image.expand_chunk( &mut self.value_reader, - &mut result.as_buffer(0).as_bytes_mut()[buffer_offset..], + &mut buffer[buffer_offset..], output_row_stride, chunk as u32, )?; } - Ok(result) + Ok(()) } /// Get the IFD decoder for our current image IFD. diff --git a/src/error.rs b/src/error.rs index 64ef5bb..c3ebefe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -184,6 +184,9 @@ quick_error! { PredictorUnavailable { display("the requested predictor is not available") } + InsufficientOutputBufferSize { needed: usize, provided: usize } { + display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}") + } } } From 5a6b2ad8f3d1556e4c92d0dc901ad1eaef6f0eb3 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:05:07 +0200 Subject: [PATCH 2/7] Complementary methods reading into borrowed buffer --- src/decoder/mod.rs | 80 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 4576868..780a22c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -251,6 +251,20 @@ impl DecodingExtent { } .map_err(overflow) } + + fn assert_layout(self, buffer: usize) -> TiffResult<()> { + let needed_bytes = self.preferred_layout()?.size(); + if buffer < needed_bytes { + Err(TiffError::UsageError( + UsageError::InsufficientOutputBufferSize { + needed: needed_bytes, + provided: buffer, + }, + )) + } else { + Ok(()) + } + } } #[derive(Debug, Copy, Clone, PartialEq)] @@ -892,6 +906,15 @@ impl Decoder { mut buffer: DecodingBuffer, chunk_index: u32, output_width: usize, + ) -> TiffResult<()> { + self.read_chunk_to_bytes(buffer.as_bytes_mut(), chunk_index, output_width) + } + + fn read_chunk_to_bytes( + &mut self, + buffer: &mut [u8], + chunk_index: u32, + output_width: usize, ) -> TiffResult<()> { let offset = self.image.chunk_file_range(chunk_index)?.0; self.goto_offset_u64(offset)?; @@ -903,7 +926,7 @@ impl Decoder { self.image.expand_chunk( &mut self.value_reader, - buffer.as_bytes_mut(), + buffer, output_row_stride.try_into()?, chunk_index, )?; @@ -973,6 +996,17 @@ impl Decoder { }) } + /// Returns the layout required to read the specified chunk with [`Self::read_chunk_bytes`]. + /// + /// Returns the layout without being specific as to the underlying type for forward + /// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of + /// channels of individual *bit* length and format each. + pub fn image_chunk_buffer_layout(&mut self, chunk_index: u32) -> TiffResult { + let data_dims = self.image().chunk_data_dimensions(chunk_index)?; + self.result_extent(data_dims.0 as usize, data_dims.1 as usize)? + .preferred_layout() + } + /// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector. pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; @@ -984,6 +1018,21 @@ impl Decoder { Ok(result) } + /// Read the specified chunk (at index `chunk_index`) into an allocated buffer. + /// + /// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a + /// call to [`Self::image_chunk_buffer_layout`]. Note that the alignment may be arbitrary, but + /// an alignment smaller than the preferred alignment may perform worse. + pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> { + let data_dims = self.image().chunk_data_dimensions(chunk_index)?; + + let extent = self.result_extent(data_dims.0 as usize, data_dims.1 as usize)?; + extent.assert_layout(buffer.len())?; + self.read_chunk_to_bytes(buffer, chunk_index, data_dims.0 as usize)?; + + Ok(()) + } + /// Returns the default chunk size for the current image. Any given chunk in the image is at most as large as /// the value returned here. For the size of the data (chunk minus padding), use `chunk_data_dimensions`. pub fn chunk_dimensions(&self) -> (u32, u32) { @@ -998,6 +1047,19 @@ impl Decoder { .expect("invalid chunk_index") } + /// Returns the preferred buffer required to read the whole image with [`Self::read_image_bytes`]. + /// + /// Returns the layout without being specific as to the underlying type for forward + /// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of + /// channels of individual *bit* length and format each. + pub fn image_buffer_layout(&mut self) -> TiffResult { + let width = self.image().width; + let height = self.image().height; + + self.result_extent(width as usize, height as usize)? + .preferred_layout() + } + /// Decodes the entire image and return it as a Vector pub fn read_image(&mut self) -> TiffResult { let width = self.image().width; @@ -1009,21 +1071,17 @@ impl Decoder { Ok(result) } + /// Decodes the entire image into a provided buffer. + /// + /// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a + /// call to [`Self::image_buffer_layout`]. Note that the alignment may be arbitrary, but an + /// alignment smaller than the preferred alignment may perform worse. pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { let width = self.image().width; let height = self.image().height; let extent = self.result_extent(width as usize, height as usize)?; - let needed_bytes = extent.preferred_layout()?.size(); - - if buffer.len() < needed_bytes { - return Err(TiffError::UsageError( - UsageError::InsufficientOutputBufferSize { - needed: needed_bytes, - provided: buffer.len(), - }, - )); - } + extent.assert_layout(buffer.len())?; if width == 0 || height == 0 { return Ok(()); From 59d32a66a75b11f905ec8c3cd575cedf458200fc Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:49:55 +0200 Subject: [PATCH 3/7] Add test coverage for to byte decoding --- Cargo.toml | 2 ++ src/decoder/mod.rs | 4 +++ tests/decode_images.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ae4f4e3..22d4718 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ zune-jpeg = { version = "0.4.17", optional = true } [dev-dependencies] criterion = "0.3.1" +# We may already depend on this via flate2 +crc32fast = "1" [features] default = ["deflate", "jpeg", "lzw"] diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 780a22c..0587385 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1001,6 +1001,8 @@ impl Decoder { /// Returns the layout without being specific as to the underlying type for forward /// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of /// channels of individual *bit* length and format each. + /// + /// See [`Self::colortype`] to describe the sample types. pub fn image_chunk_buffer_layout(&mut self, chunk_index: u32) -> TiffResult { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; self.result_extent(data_dims.0 as usize, data_dims.1 as usize)? @@ -1052,6 +1054,8 @@ impl Decoder { /// Returns the layout without being specific as to the underlying type for forward /// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of /// channels of individual *bit* length and format each. + /// + /// See [`Self::colortype`] to describe the sample types. pub fn image_buffer_layout(&mut self) -> TiffResult { let width = self.image().width; let height = self.image().height; diff --git a/tests/decode_images.rs b/tests/decode_images.rs index 5a62279..bc93640 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -522,3 +522,60 @@ fn test_zstd_compression() { // gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396); } + +fn test_image_bytes(file: &str, expected: &[(ColorType, u32)]) { + let path = PathBuf::from(TEST_IMAGE_DIR).join(file); + let img_file = File::open(path).expect("Cannot find test image!"); + let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); + + let mut buffer = vec![0u8; 0]; + for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() { + if idx > 0 { + decoder + .next_image() + .expect("mismatched number of directories"); + } + + assert_eq!(decoder.colortype().unwrap(), expected_type); + let layout = decoder + .image_buffer_layout() + .expect("Cannot describe buffer layout"); + + buffer.resize(layout.size(), 0u8); + decoder.read_image_bytes(&mut buffer).unwrap(); + + // Well almost a crc.. + assert_eq!(crc32fast::hash(&buffer), expected_crc); + } +} + +#[test] +fn bytes_cmyk_3c_8b() { + test_image_bytes("cmyk-3c-8b.tiff", &[(ColorType::CMYK(8), 2935032230)]); +} + +#[test] +fn bytes_gray_16b() { + test_image_bytes( + "minisblack-1c-16b.tiff", + &[(ColorType::Gray(16), 3524597615)], + ); +} + +#[test] +fn bytes_gray_32b() { + test_image_bytes("gradient-1c-32b.tiff", &[(ColorType::Gray(32), 2257036530)]); +} + +#[test] +fn bytes_gray_u64() { + test_image_bytes("gradient-1c-64b.tiff", &[(ColorType::Gray(64), 2156547113)]); +} + +#[test] +fn bytes_gray_f32() { + test_image_bytes( + "gradient-1c-32b-float.tiff", + &[(ColorType::Gray(32), 3232356730)], + ); +} From c370ac4b923766c21ff1d9ec20836a4831c369d8 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 19 Jul 2025 00:12:43 +0200 Subject: [PATCH 4/7] Take row stride input in chunk bytes --- src/decoder/mod.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 0587385..1228335 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -907,23 +907,23 @@ impl Decoder { chunk_index: u32, output_width: usize, ) -> TiffResult<()> { - self.read_chunk_to_bytes(buffer.as_bytes_mut(), chunk_index, output_width) + let output_row_stride = (output_width as u64) + .saturating_mul(self.image.samples_per_pixel() as u64) + .saturating_mul(self.image.bits_per_sample as u64) + .div_ceil(8); + + self.read_chunk_to_bytes(buffer.as_bytes_mut(), chunk_index, output_row_stride) } fn read_chunk_to_bytes( &mut self, buffer: &mut [u8], chunk_index: u32, - output_width: usize, + output_row_stride: u64, ) -> TiffResult<()> { let offset = self.image.chunk_file_range(chunk_index)?.0; self.goto_offset_u64(offset)?; - let output_row_stride = (output_width as u64) - .saturating_mul(self.image.samples_per_pixel() as u64) - .saturating_mul(self.image.bits_per_sample as u64) - / 8; - self.image.expand_chunk( &mut self.value_reader, buffer, @@ -1030,7 +1030,13 @@ impl Decoder { let extent = self.result_extent(data_dims.0 as usize, data_dims.1 as usize)?; extent.assert_layout(buffer.len())?; - self.read_chunk_to_bytes(buffer, chunk_index, data_dims.0 as usize)?; + + let output_row_stride = u64::from(data_dims.0) + .saturating_mul(self.image.samples_per_pixel() as u64) + .saturating_mul(self.image.bits_per_sample as u64) + .div_ceil(8); + + self.read_chunk_to_bytes(buffer, chunk_index, output_row_stride)?; Ok(()) } From 48d52bed9bb899df0ca42ec3da788c98d59b8ca9 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:21:52 +0200 Subject: [PATCH 5/7] Return buffer layout as a struct Prepares extensibility for the eventual case of supporting multi-planar image layouts that are to be written into a multi-planar buffer as indicated by the caller in some way (possibly not via read_*_bytes but a method taking a wrapper around the output buffer instead). Through the struct we can later add an indicator that can be used by the caller to decide between methods of fetching the pixel data as appropriate. --- src/decoder/image.rs | 17 ++++++++++ src/decoder/mod.rs | 72 ++++++++++++++++++++++++++++++++---------- tests/decode_images.rs | 2 +- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 49510a3..1145e5c 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -10,6 +10,7 @@ use crate::{ ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError, }; use std::io::{self, Cursor, Read, Seek}; +use std::num::NonZeroUsize; use std::sync::Arc; #[derive(Debug)] @@ -363,6 +364,22 @@ impl Image { } } + pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option { + let (width, height) = dims; + + let row_stride = u64::from(width) + .saturating_mul(self.samples_per_pixel() as u64) + .saturating_mul(self.bits_per_sample as u64) + .div_ceil(8); + + // Note: row stride should be smaller than the len if we have an actual buffer. If there + // are no pixels in the buffer (height _or_ width is 0) then the stride is not well defined + // and we return `None`. + (height > 0) + .then_some(row_stride as usize) + .and_then(NonZeroUsize::new) + } + fn create_reader<'r, R: 'r + Read>( reader: R, compression_method: CompressionMethod, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 1228335..cac2591 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,6 +1,7 @@ use std::alloc::{Layout, LayoutError}; use std::collections::BTreeMap; use std::io::{self, Read, Seek}; +use std::num::NonZeroUsize; use crate::tags::{ CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, @@ -198,6 +199,19 @@ impl<'a> DecodingBuffer<'a> { } } +/// Information on the byte buffer that should be supplied to the decoder. +/// +/// This is relevant for [`Decoder::read_image_bytes`] and [`Decoder::read_chunk_bytes`] where the +/// caller provided buffer must fit the expectations of the decoder to be filled with data from the +/// current image. +#[non_exhaustive] +pub struct BufferLayoutPreference { + /// Minimum byte size of the buffer to read image data. + pub len: usize, + /// Minimum number of bytes to represent a row of image data of the requested content. + pub row_stride: Option, +} + /// The count and matching discriminant for a `DecodingBuffer`. #[derive(Clone)] enum DecodingExtent { @@ -939,26 +953,32 @@ impl Decoder { .to_result_buffer(&self.value_reader.limits) } + // FIXME: it's unusual for this method to take a `usize` dimension parameter since those would + // typically come from the image data. The preferred consistent representation should be `u32` + // and that would avoid some unusual casts `usize -> u64` with a `u64::from` instead. fn result_extent(&self, width: usize, height: usize) -> TiffResult { - let bits_per_sample = self.image().bits_per_sample; + let bits_per_sample = self.image.bits_per_sample; + // If samples take up more than one byte, we expand it to a full number (integer or float) + // and the number of samples in that integer type just counts the number in the underlying + // image itself. Otherwise, a row is represented as the byte slice of bitfields without + // expansion. let row_samples = if bits_per_sample >= 8 { width } else { - ((((width as u64) * bits_per_sample as u64) + 7) / 8) + ((width as u64) * bits_per_sample as u64) + .div_ceil(8) .try_into() .map_err(|_| TiffError::LimitsExceeded)? }; let buffer_size = row_samples .checked_mul(height) - .and_then(|x| x.checked_mul(self.image().samples_per_pixel())) + .and_then(|x| x.checked_mul(self.image.samples_per_pixel())) .ok_or(TiffError::LimitsExceeded)?; - let max_sample_bits = self.image().bits_per_sample; - Ok(match self.image().sample_format { - SampleFormat::Uint => match max_sample_bits { + SampleFormat::Uint => match bits_per_sample { n if n <= 8 => DecodingExtent::U8(buffer_size), n if n <= 16 => DecodingExtent::U16(buffer_size), n if n <= 32 => DecodingExtent::U32(buffer_size), @@ -969,7 +989,7 @@ impl Decoder { )) } }, - SampleFormat::IEEEFP => match max_sample_bits { + SampleFormat::IEEEFP => match bits_per_sample { 16 => DecodingExtent::F16(buffer_size), 32 => DecodingExtent::F32(buffer_size), 64 => DecodingExtent::F64(buffer_size), @@ -979,7 +999,7 @@ impl Decoder { )) } }, - SampleFormat::Int => match max_sample_bits { + SampleFormat::Int => match bits_per_sample { n if n <= 8 => DecodingExtent::I8(buffer_size), n if n <= 16 => DecodingExtent::I16(buffer_size), n if n <= 32 => DecodingExtent::I32(buffer_size), @@ -996,17 +1016,28 @@ impl Decoder { }) } - /// Returns the layout required to read the specified chunk with [`Self::read_chunk_bytes`]. + /// Returns the layout preferred to read the specified chunk with [`Self::read_chunk_bytes`]. /// /// Returns the layout without being specific as to the underlying type for forward /// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of /// channels of individual *bit* length and format each. /// /// See [`Self::colortype`] to describe the sample types. - pub fn image_chunk_buffer_layout(&mut self, chunk_index: u32) -> TiffResult { + pub fn image_chunk_buffer_layout( + &mut self, + chunk_index: u32, + ) -> TiffResult { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; - self.result_extent(data_dims.0 as usize, data_dims.1 as usize)? - .preferred_layout() + let layout = self + .result_extent(data_dims.0 as usize, data_dims.1 as usize)? + .preferred_layout()?; + + let row_stride = self.image.minimum_row_stride(data_dims); + + Ok(BufferLayoutPreference { + len: layout.size(), + row_stride, + }) } /// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector. @@ -1062,12 +1093,19 @@ impl Decoder { /// channels of individual *bit* length and format each. /// /// See [`Self::colortype`] to describe the sample types. - pub fn image_buffer_layout(&mut self) -> TiffResult { - let width = self.image().width; - let height = self.image().height; + pub fn image_buffer_layout(&mut self) -> TiffResult { + let data_dims @ (width, height) = (self.image.width, self.image.height); - self.result_extent(width as usize, height as usize)? - .preferred_layout() + let layout = self + .result_extent(width as usize, height as usize)? + .preferred_layout()?; + + let row_stride = self.image.minimum_row_stride(data_dims); + + Ok(BufferLayoutPreference { + len: layout.size(), + row_stride, + }) } /// Decodes the entire image and return it as a Vector diff --git a/tests/decode_images.rs b/tests/decode_images.rs index bc93640..fc4b8eb 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -541,7 +541,7 @@ fn test_image_bytes(file: &str, expected: &[(ColorType, u32)]) { .image_buffer_layout() .expect("Cannot describe buffer layout"); - buffer.resize(layout.size(), 0u8); + buffer.resize(layout.len, 0u8); decoder.read_image_bytes(&mut buffer).unwrap(); // Well almost a crc.. From f671293bd8c90e360b1bf91f9007894d39fb6fa6 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:08:02 +0200 Subject: [PATCH 6/7] Fix byte-crc test for big endian platforms --- tests/decode_images.rs | 49 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/tests/decode_images.rs b/tests/decode_images.rs index fc4b8eb..b15ffb2 100644 --- a/tests/decode_images.rs +++ b/tests/decode_images.rs @@ -523,7 +523,12 @@ fn test_zstd_compression() { test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396); } -fn test_image_bytes(file: &str, expected: &[(ColorType, u32)]) { +fn test_image_bytes( + file: &str, + expected: &[(ColorType, u32)], + // Represent all samples as little endian, ensuring check sums on all platforms. + normalize_byte_order: fn(&mut [u8]), +) { let path = PathBuf::from(TEST_IMAGE_DIR).join(file); let img_file = File::open(path).expect("Cannot find test image!"); let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); @@ -543,15 +548,41 @@ fn test_image_bytes(file: &str, expected: &[(ColorType, u32)]) { buffer.resize(layout.len, 0u8); decoder.read_image_bytes(&mut buffer).unwrap(); + normalize_byte_order(&mut buffer); // Well almost a crc.. assert_eq!(crc32fast::hash(&buffer), expected_crc); } } +fn byte_order_u16(bytes: &mut [u8]) { + for chunk in bytes.chunks_exact_mut(2) { + let n = u16::from_ne_bytes(chunk.try_into().unwrap()); + chunk.copy_from_slice(&n.to_le_bytes()); + } +} + +fn byte_order_u32(bytes: &mut [u8]) { + for chunk in bytes.chunks_exact_mut(4) { + let n = u32::from_ne_bytes(chunk.try_into().unwrap()); + chunk.copy_from_slice(&n.to_le_bytes()); + } +} + +fn byte_order_u64(bytes: &mut [u8]) { + for chunk in bytes.chunks_exact_mut(8) { + let n = u64::from_ne_bytes(chunk.try_into().unwrap()); + chunk.copy_from_slice(&n.to_le_bytes()); + } +} + #[test] fn bytes_cmyk_3c_8b() { - test_image_bytes("cmyk-3c-8b.tiff", &[(ColorType::CMYK(8), 2935032230)]); + test_image_bytes( + "cmyk-3c-8b.tiff", + &[(ColorType::CMYK(8), 2935032230)], + |_| (), + ); } #[test] @@ -559,17 +590,26 @@ fn bytes_gray_16b() { test_image_bytes( "minisblack-1c-16b.tiff", &[(ColorType::Gray(16), 3524597615)], + byte_order_u16, ); } #[test] fn bytes_gray_32b() { - test_image_bytes("gradient-1c-32b.tiff", &[(ColorType::Gray(32), 2257036530)]); + test_image_bytes( + "gradient-1c-32b.tiff", + &[(ColorType::Gray(32), 2257036530)], + byte_order_u32, + ); } #[test] fn bytes_gray_u64() { - test_image_bytes("gradient-1c-64b.tiff", &[(ColorType::Gray(64), 2156547113)]); + test_image_bytes( + "gradient-1c-64b.tiff", + &[(ColorType::Gray(64), 2156547113)], + byte_order_u64, + ); } #[test] @@ -577,5 +617,6 @@ fn bytes_gray_f32() { test_image_bytes( "gradient-1c-32b-float.tiff", &[(ColorType::Gray(32), 3232356730)], + byte_order_u32, ); } From d70c31dcbf7c1b7c6377902f6ec09c79ccb0a521 Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:13:41 +0200 Subject: [PATCH 7/7] Add a Bug note about planar configuration --- src/decoder/mod.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index cac2591..0299047 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1023,6 +1023,12 @@ impl Decoder { /// channels of individual *bit* length and format each. /// /// See [`Self::colortype`] to describe the sample types. + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only + /// indicate the layout needed to read the first data plane. This will be fixed in a future + /// major version of `tiff`. pub fn image_chunk_buffer_layout( &mut self, chunk_index: u32, @@ -1041,6 +1047,11 @@ impl Decoder { } /// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector. + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only read + /// the first sample's plane. This will be fixed in a future major version of `tiff`. pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; @@ -1056,6 +1067,11 @@ impl Decoder { /// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a /// call to [`Self::image_chunk_buffer_layout`]. Note that the alignment may be arbitrary, but /// an alignment smaller than the preferred alignment may perform worse. + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only read + /// the first sample's plane. This will be fixed in a future major version of `tiff`. pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; @@ -1093,6 +1109,12 @@ impl Decoder { /// channels of individual *bit* length and format each. /// /// See [`Self::colortype`] to describe the sample types. + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only + /// indicate the layout needed to read the first data plane. This will be fixed in a future + /// major version of `tiff`. pub fn image_buffer_layout(&mut self) -> TiffResult { let data_dims @ (width, height) = (self.image.width, self.image.height); @@ -1109,6 +1131,11 @@ impl Decoder { } /// Decodes the entire image and return it as a Vector + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only read + /// the first sample's plane. This will be fixed in a future major version of `tiff`. pub fn read_image(&mut self) -> TiffResult { let width = self.image().width; let height = self.image().height; @@ -1124,6 +1151,11 @@ impl Decoder { /// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a /// call to [`Self::image_buffer_layout`]. Note that the alignment may be arbitrary, but an /// alignment smaller than the preferred alignment may perform worse. + /// + /// # Bugs + /// + /// When the image is stored as a planar configuration, this method will currently only read + /// the first sample's plane. This will be fixed in a future major version of `tiff`. pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { let width = self.image().width; let height = self.image().height;