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] 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..