Sample types in layouts, resize DecoderResult

By adding the type interpretation to `BufferLayoutPreference` (simply
computed from the same fields that are trivial to fetch in
`ReadoutLayout` with the rest) we can allocate an appropriate
`DecoderResult` without any `Decoder` instance. We still want a Limits
instance though. This lets a caller read _typed_ data from the `_bytes`
methods which perform less allocations. It also lets them read multiple
planes in a more convenient interface. See, `read_image` will keep its
planar treatment bug for a little bit so you don't accidentally break
during an upgrade.

This also implies less code is being monomorphized, just a bonus.
This commit is contained in:
A. Molzer committed 2025-12-28 21:06:59 +01:00
1 parent af8a8d660f
commit 6d56d014a1
3 files changed
+163 -73

No files matched your search

+6 -1
View File
@@ -10,12 +10,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let io = std::io::BufReader::new(file); let io = std::io::BufReader::new(file);
let mut reader = Decoder::new(io)?; let mut reader = Decoder::new(io)?;
let mut data = DecodingResult::I8(vec![]);
let limits = tiff::decoder::Limits::unlimited();
for i in 0u32.. { for i in 0u32.. {
let layout = reader.image_buffer_layout()?; let layout = reader.image_buffer_layout()?;
let colortype = reader.colortype()?; let colortype = reader.colortype()?;
let dimensions = reader.dimensions()?; let dimensions = reader.dimensions()?;
let mut data = reader.read_image()?; data.resize_to(&layout, &limits)?;
reader.read_image_bytes(data.as_buffer(0).as_bytes_mut())?;
debug_planes(i, &mut data, &layout, &colortype, dimensions); debug_planes(i, &mut data, &layout, &colortype, dimensions);
if !reader.more_images() { if !reader.more_images() {
+3
View File
@@ -98,6 +98,8 @@ pub(crate) struct ReadoutLayout {
pub color: ColorType, pub color: ColorType,
/// The number of bytes from one row to another. /// The number of bytes from one row to another.
pub minimum_row_stride: usize, pub minimum_row_stride: usize,
/// The format of samples (assumed uniform for now, same with depth of `ColorType`).
pub sample_format: SampleFormat,
/// Number of bytes to advance in output per row. /// Number of bytes to advance in output per row.
pub row_stride: usize, pub row_stride: usize,
@@ -659,6 +661,7 @@ impl Image {
planar_config: self.planar_config, planar_config: self.planar_config,
color, color,
minimum_row_stride, minimum_row_stride,
sample_format: self.sample_format,
row_stride: data_row_bytes, row_stride: data_row_bytes,
chunk_row_stride: chunk_row_bytes, chunk_row_stride: chunk_row_bytes,
chunk_col_stride, chunk_col_stride,
+154 -72
View File
@@ -54,6 +54,17 @@ pub enum DecodingResult {
} }
impl DecodingResult { impl DecodingResult {
/// Reallocate the buffer to decode all planes of the indicated layout.
pub fn resize_to(
&mut self,
buffer: &BufferLayoutPreference,
limits: &Limits,
) -> Result<(), TiffError> {
let extent = DecodingExtent::U8(buffer.complete_len);
*self = extent.to_result_buffer(limits)?;
Ok(())
}
fn new<T: Default + Copy>( fn new<T: Default + Copy>(
size: usize, size: usize,
limits: &Limits, limits: &Limits,
@@ -192,6 +203,39 @@ impl<'a> DecodingBuffer<'a> {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DecodingSampleType {
U8,
U16,
U32,
U64,
F16,
F32,
F64,
I8,
I16,
I32,
I64,
}
impl DecodingSampleType {
fn extent_for_bytes(self, bytes: usize) -> DecodingExtent {
match self {
DecodingSampleType::U8 => DecodingExtent::U8(bytes),
DecodingSampleType::U16 => DecodingExtent::U16(bytes.div_ceil(2)),
DecodingSampleType::U32 => DecodingExtent::U32(bytes.div_ceil(4)),
DecodingSampleType::U64 => DecodingExtent::U64(bytes.div_ceil(8)),
DecodingSampleType::I8 => DecodingExtent::I8(bytes),
DecodingSampleType::I16 => DecodingExtent::I16(bytes.div_ceil(2)),
DecodingSampleType::I32 => DecodingExtent::I32(bytes.div_ceil(4)),
DecodingSampleType::I64 => DecodingExtent::I64(bytes.div_ceil(8)),
DecodingSampleType::F16 => DecodingExtent::F16(bytes.div_ceil(2)),
DecodingSampleType::F32 => DecodingExtent::F32(bytes.div_ceil(4)),
DecodingSampleType::F64 => DecodingExtent::F64(bytes.div_ceil(8)),
}
}
}
/// Information on the byte buffer that should be supplied to the decoder. /// 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 /// This is relevant for [`Decoder::read_image_bytes`] and [`Decoder::read_chunk_bytes`] where the
@@ -202,6 +246,15 @@ impl<'a> DecodingBuffer<'a> {
pub struct BufferLayoutPreference { pub struct BufferLayoutPreference {
/// Minimum byte size of the buffer to read image data. /// Minimum byte size of the buffer to read image data.
pub len: usize, pub len: usize,
/// The interpretation of each sample in the image.
///
/// We only support a uniform sample layout. Detailed information for mixed colors may be added
/// in the future and will become available by explicit query. The same goes for the bit-depth
/// of samples that must also be uniform.
pub sample_format: SampleFormat,
/// The type representation for each sample. Only available for depths and formats which the
/// library can describe.
pub sample_type: Option<DecodingSampleType>,
/// Minimum number of bytes to represent a row of image data of the requested content. /// Minimum number of bytes to represent a row of image data of the requested content.
pub row_stride: Option<NonZeroUsize>, pub row_stride: Option<NonZeroUsize>,
/// Number of planes in the image. /// Number of planes in the image.
@@ -220,11 +273,87 @@ impl BufferLayoutPreference {
planes: layout.plane_offsets.len(), planes: layout.plane_offsets.len(),
plane_stride: core::num::NonZeroUsize::new(layout.readout.plane_stride), plane_stride: core::num::NonZeroUsize::new(layout.readout.plane_stride),
complete_len: layout.total_bytes, complete_len: layout.total_bytes,
sample_format: layout.readout.sample_format,
sample_type: Self::sample_type(layout.readout.sample_format, layout.readout.color),
} }
} }
fn sample_type(sample_format: SampleFormat, color: ColorType) -> Option<DecodingSampleType> {
Some(match sample_format {
SampleFormat::Uint => match color.bit_depth() {
n if n <= 8 => DecodingSampleType::U8,
n if n <= 16 => DecodingSampleType::U16,
n if n <= 32 => DecodingSampleType::U32,
n if n <= 64 => DecodingSampleType::U64,
_ => return None,
},
SampleFormat::IEEEFP => match color.bit_depth() {
16 => DecodingSampleType::F16,
32 => DecodingSampleType::F32,
64 => DecodingSampleType::F64,
_ => return None,
},
SampleFormat::Int => match color.bit_depth() {
n if n <= 8 => DecodingSampleType::I8,
n if n <= 16 => DecodingSampleType::I16,
n if n <= 32 => DecodingSampleType::I32,
n if n <= 64 => DecodingSampleType::I64,
_ => return None,
},
_other => {
return None;
}
})
}
} }
impl image::ReadoutLayout { impl image::ReadoutLayout {
// FIXME: when planes are not homogenous (i.e. subsampled or differing depths) then the
// `readout_for_size` or `to_plane_layout` needs a parameter to determine the planes being
// read instead of assuming a constant repeated size for them.
fn result_extent_for_planes(
self: &image::ReadoutLayout,
planes: core::ops::Range<u16>,
) -> TiffResult<DecodingExtent> {
let buffer = self.to_plane_layout()?;
// The layout is for all planes. So restrict ourselves to the planes that were requested.
let offset = match buffer.plane_offsets.get(usize::from(planes.start)) {
Some(n) => *n,
None => {
return Err(TiffError::UsageError(UsageError::InvalidPlaneIndex(
planes.start,
)))
}
};
let end = match buffer.plane_offsets.get(usize::from(planes.end)) {
Some(n) => *n,
None => buffer.total_bytes,
};
let buffer_bytes = end - offset;
let bits_per_sample = self.color.bit_depth();
let Some(sample_type) = BufferLayoutPreference::sample_type(self.sample_format, self.color)
else {
if matches!(
self.sample_format,
SampleFormat::Uint | SampleFormat::Int | SampleFormat::IEEEFP
) {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedSampleDepth(bits_per_sample),
));
} else {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedSampleFormat(vec![self.sample_format]),
));
}
};
Ok(sample_type.extent_for_bytes(buffer_bytes))
}
#[inline(always)] #[inline(always)]
fn assert_min_layout<T>(&self, buffer: &[T]) -> TiffResult<()> { fn assert_min_layout<T>(&self, buffer: &[T]) -> TiffResult<()> {
if core::mem::size_of_val(buffer) < self.plane_stride { if core::mem::size_of_val(buffer) < self.plane_stride {
@@ -293,6 +422,22 @@ impl DecodingExtent {
} }
.map_err(overflow) .map_err(overflow)
} }
fn sample_type(&self) -> DecodingSampleType {
match *self {
DecodingExtent::U8(_) => DecodingSampleType::U8,
DecodingExtent::U16(_) => DecodingSampleType::U16,
DecodingExtent::U32(_) => DecodingSampleType::U32,
DecodingExtent::U64(_) => DecodingSampleType::U64,
DecodingExtent::F16(_) => DecodingSampleType::F16,
DecodingExtent::F32(_) => DecodingSampleType::F32,
DecodingExtent::F64(_) => DecodingSampleType::F64,
DecodingExtent::I8(_) => DecodingSampleType::I8,
DecodingExtent::I16(_) => DecodingSampleType::I16,
DecodingExtent::I32(_) => DecodingSampleType::I32,
DecodingExtent::I64(_) => DecodingSampleType::I64,
}
}
} }
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
@@ -1030,73 +1175,6 @@ impl<R: Read + Seek> Decoder<R> {
Ok(()) Ok(())
} }
// FIXME: when planes are not homogenous (i.e. subsampled or differing depths) then the
// `readout_for_size` or `to_plane_layout` needs a parameter to determine the planes being
// read instead of assuming a constant repeated size for them.
fn result_extent(
&self,
read: &image::ReadoutLayout,
planes: core::ops::Range<u16>,
) -> TiffResult<DecodingExtent> {
let buffer = read.to_plane_layout()?;
// The layout is for all planes. So restrict ourselves to the planes that were requested.
let offset = match buffer.plane_offsets.get(usize::from(planes.start)) {
Some(n) => *n,
None => {
return Err(TiffError::UsageError(UsageError::InvalidPlaneIndex(
planes.start,
)))
}
};
let end = match buffer.plane_offsets.get(usize::from(planes.end)) {
Some(n) => *n,
None => buffer.total_bytes,
};
let buffer_bytes = end - offset;
let bits_per_sample = read.color.bit_depth();
Ok(match self.image().sample_format {
SampleFormat::Uint => match bits_per_sample {
n if n <= 8 => DecodingExtent::U8(buffer_bytes),
n if n <= 16 => DecodingExtent::U16(buffer_bytes / 2),
n if n <= 32 => DecodingExtent::U32(buffer_bytes / 4),
n if n <= 64 => DecodingExtent::U64(buffer_bytes / 8),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
SampleFormat::IEEEFP => match bits_per_sample {
16 => DecodingExtent::F16(buffer_bytes / 2),
32 => DecodingExtent::F32(buffer_bytes / 4),
64 => DecodingExtent::F64(buffer_bytes / 8),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
SampleFormat::Int => match bits_per_sample {
n if n <= 8 => DecodingExtent::I8(buffer_bytes),
n if n <= 16 => DecodingExtent::I16(buffer_bytes / 2),
n if n <= 32 => DecodingExtent::I32(buffer_bytes / 4),
n if n <= 64 => DecodingExtent::I64(buffer_bytes / 8),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
format => {
return Err(TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into())
}
})
}
/// Returns the layout preferred 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 /// Returns the layout without being specific as to the underlying type for forward
@@ -1117,7 +1195,9 @@ impl<R: Read + Seek> Decoder<R> {
let data_dims = self.image().chunk_data_dimensions(chunk_index)?; let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?; let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?;
let layout = self.result_extent(&readout, 0..1)?.preferred_layout()?; let extent = readout.result_extent_for_planes(0..1)?;
let sample_type = extent.sample_type();
let layout = extent.preferred_layout()?;
let row_stride = core::num::NonZeroUsize::new(readout.minimum_row_stride); let row_stride = core::num::NonZeroUsize::new(readout.minimum_row_stride);
let plane_stride = core::num::NonZeroUsize::new(readout.plane_stride); let plane_stride = core::num::NonZeroUsize::new(readout.plane_stride);
@@ -1128,6 +1208,8 @@ impl<R: Read + Seek> Decoder<R> {
planes: 1, planes: 1,
plane_stride, plane_stride,
complete_len: layout.size(), complete_len: layout.size(),
sample_format: self.image().sample_format,
sample_type: Some(sample_type),
}) })
} }
@@ -1177,8 +1259,8 @@ impl<R: Read + Seek> Decoder<R> {
let readout = self.image().readout_for_size(width, height)?; let readout = self.image().readout_for_size(width, height)?;
let mut result = self let mut result = readout
.result_extent(&readout, 0..1)? .result_extent_for_planes(0..1)?
.to_result_buffer(&self.value_reader.limits)?; .to_result_buffer(&self.value_reader.limits)?;
self.read_chunk_to_bytes(result.as_buffer(0).as_bytes_mut(), chunk_index, &readout)?; self.read_chunk_to_bytes(result.as_buffer(0).as_bytes_mut(), chunk_index, &readout)?;
@@ -1303,8 +1385,8 @@ impl<R: Read + Seek> Decoder<R> {
pub fn read_image(&mut self) -> TiffResult<DecodingResult> { pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let readout = self.image().readout_for_image()?; let readout = self.image().readout_for_image()?;
let mut result = self let mut result = readout
.result_extent(&readout, 0..1)? .result_extent_for_planes(0..1)?
.to_result_buffer(&self.value_reader.limits)?; .to_result_buffer(&self.value_reader.limits)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?; self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;