diff --git a/examples/decode.rs b/examples/decode.rs new file mode 100644 index 0000000..18eaa79 --- /dev/null +++ b/examples/decode.rs @@ -0,0 +1,103 @@ +use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult}; + +fn main() -> Result<(), Box> { + let Some(image) = std::env::args_os().nth(1) else { + eprintln!("Usage: decode FILE"); + return Ok(()); + }; + + let file = std::fs::File::open(image)?; + let io = std::io::BufReader::new(file); + let mut reader = Decoder::new(io)?; + + let mut data = DecodingResult::I8(vec![]); + + for i in 0u32.. { + let colortype = reader.colortype()?; + let dimensions = reader.dimensions()?; + let layout = reader.read_image_to_buffer(&mut data)?; + + debug_planes(i, &mut data, &layout, &colortype, dimensions); + + if !reader.more_images() { + break; + } + + reader.next_image()? + } + + Ok(()) +} + +fn debug_planes( + index: u32, + data: &mut DecodingResult, + layout: &BufferLayoutPreference, + colortype: &tiff::ColorType, + (width, height): (u32, u32), +) { + let (depth, mut tupltype) = match colortype { + tiff::ColorType::Gray(_) => (1, "GRAYSCALE"), + tiff::ColorType::RGB(_) => (3, "RGB"), + tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"), + tiff::ColorType::Palette(_) => (1, "PALETTE"), + tiff::ColorType::CMYK(_) => (4, "CMYK"), + tiff::ColorType::Multiband { num_samples: 2, .. } => (2, "GRAYSCALE_ALPHA"), + _ => { + eprintln!("Unsupported color type for PAM output: {:?}", colortype); + return; + } + }; + + let maxval = match data.as_buffer(0) { + DecodingBuffer::U8(_) => u64::from(u8::MAX), + DecodingBuffer::U16(_) => u64::from(u16::MAX), + DecodingBuffer::U32(_) => u64::from(u32::MAX), + DecodingBuffer::U64(_) => u64::MAX, + // Eh, this is a good effort I guess. + DecodingBuffer::I8(items) => { + items.iter_mut().for_each(|v| *v = 0.max(*v)); + u64::from(i8::MAX as u8) + } + DecodingBuffer::I16(items) => { + items.iter_mut().for_each(|v| *v = 0.max(*v)); + u64::from(i16::MAX as u16) + } + DecodingBuffer::I32(items) => { + items.iter_mut().for_each(|v| *v = 0.max(*v)); + u64::from(i32::MAX as u32) + } + DecodingBuffer::I64(items) => { + items.iter_mut().for_each(|v| *v = 0.max(*v)); + i64::MAX as u64 + } + // Not supported by PAM. + DecodingBuffer::F16(_) | DecodingBuffer::F32(_) | DecodingBuffer::F64(_) => return, + }; + + let byte_len = if layout.planes > 1 { + tupltype = "GRAYSCALE"; + layout.plane_stride.unwrap().get() + } else { + data.as_buffer(0).byte_len() + }; + + // Write out this image's planes to PAM files. + let header = format!("P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {depth}\nMAXVAL {maxval}\nTUPLTYPE {tupltype}\nENDHDR\n"); + + for (plane, plane_data) in data + .as_buffer(0) + .as_bytes_mut() + .chunks_exact(byte_len) + .enumerate() + { + let filename = format!("image_{index:03}_plane_{plane:03}.pam"); + let mut file = std::fs::File::create(filename).expect("Failed to create output file"); + + use std::io::Write; + file.write_all(header.as_bytes()) + .expect("Failed to write PAM header"); + file.write_all(plane_data) + .expect("Failed to write PAM data"); + } +} diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 05e82e1..4ff351b 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -12,7 +12,6 @@ use crate::{ }; use std::io::{self, Cursor, Read, Seek}; -use std::num::NonZeroUsize; use std::sync::Arc; #[derive(Debug)] @@ -88,6 +87,44 @@ pub(crate) struct Image { pub chunk_bytes: Vec, } +/// Describes how to read a tile-aligned portion of the image. +#[derive(Clone)] +pub(crate) struct ReadoutLayout { + /// The planar configuration, which applies to both the underlying image and the output buffer. + /// This may be relaxed if we find a clean enough way to provide it. + pub planar_config: PlanarConfiguration, + + /// The sample interpretation (interpret with planar_config). + pub color: ColorType, + /// The number of bytes from one row to another. + 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. + pub row_stride: usize, + /// Number of bytes to advance in output per chunk in width. + pub chunk_row_stride: usize, + /// Number of bytes to advance in output per chunk in height. + pub chunk_col_stride: usize, + /// Number of bytes in output from one plane to another. + pub plane_stride: usize, + + /// Bits per sample in the encoded data. + pub tiff_bits_per_sample: u8, + /// Number of samples in the encoded data. + pub tiff_samples: u16, + /// Dimensions of the underlying rectangular chunks (tile or strips). + pub tiff_chunk_dimensions: (u32, u32), + /// Number of bytes in the underlying data with all samples per row of chunks. + pub tiff_row_bytes: usize, + + /// Chunks until wrapping to the next row of chunks. + pub chunks_across: u32, + /// Chunks to advance to get to the next plane of chunks. + pub chunks_per_plane: u32, +} + impl Image { pub fn from_reader( decoder: &mut ValueReader, @@ -190,7 +227,7 @@ impl Image { // Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows // it to be a single value that applies to all samples. - if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 { + if bits_per_sample.len() != usize::from(samples) && bits_per_sample.len() != 1 { return Err(TiffError::FormatError( TiffFormatError::InconsistentSizesEncountered, )); @@ -342,7 +379,7 @@ impl Image { pub(crate) fn colortype(&self) -> TiffResult { let is_alpha_extra_samples = matches!( self.extra_samples.as_slice(), - [ExtraSamples::AssociatedAlpha] | [ExtraSamples::UnassociatedAlpha] + [ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..] ); match self.photometric_interpretation { @@ -410,38 +447,6 @@ impl Image { } } - /// Get the multiband color describing this with its extra samples. - pub(crate) fn color_multiband_with_extras(&self) -> ColorType { - ColorType::Multiband { - bit_depth: self.bits_per_sample, - num_samples: self.samples, - } - } - - /// Describe this with an accurate color or a multiband. - pub(crate) fn color_or_fallback(&self) -> ColorType { - self.colortype() - .unwrap_or_else(|_| self.color_multiband_with_extras()) - } - - pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option { - let (width, height) = dims; - - let color = self.color_or_fallback(); - - let row_stride = u64::from(width) - .saturating_mul(u64::from(self.samples_per_out_texel(color))) - .saturating_mul(u64::from(self.bits_per_sample)) - .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, @@ -538,9 +543,9 @@ impl Image { /// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`: /// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...) /// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...) - pub(crate) fn samples_per_pixel(&self) -> usize { + pub(crate) fn samples_per_pixel(&self) -> u16 { match self.planar_config { - PlanarConfiguration::Chunky => self.samples.into(), + PlanarConfiguration::Chunky => self.samples, PlanarConfiguration::Planar => 1, } } @@ -553,10 +558,10 @@ impl Image { } /// Number of strips per pixel. - pub(crate) fn strips_per_pixel(&self) -> usize { + pub(crate) fn strips_per_pixel(&self) -> u16 { match self.planar_config { PlanarConfiguration::Chunky => 1, - PlanarConfiguration::Planar => self.samples.into(), + PlanarConfiguration::Planar => self.samples, } } @@ -594,14 +599,90 @@ impl Image { } } + pub(crate) fn readout_for_image(&self) -> TiffResult { + let Image { width, height, .. } = *self; + self.readout_for_size(width, height) + } + + /// Get the layout for reading out a tile-aligned portion of the image. + /// + /// The provided width and height should be less than or equal to the image dimensions. + pub(crate) fn readout_for_size(&self, width: u32, height: u32) -> TiffResult { + let color = self.colortype()?; + + let tiff_samples = self.samples_per_pixel(); + let tiff_bits_per_sample = self.bits_per_sample; + let data_samples = self.samples_per_out_texel(color); + let tiff_chunk_dimensions = self.chunk_dimensions()?; + let strips_per_pixel = self.strips_per_pixel(); + + let data_dimensions = (width, height); + + let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) + .checked_mul(u64::from(tiff_samples)) + .ok_or(TiffError::LimitsExceeded)?; + let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?; + + let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample)) + .checked_mul(u64::from(data_samples)) + .ok_or(TiffError::LimitsExceeded)?; + let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?; + + let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample)) + .checked_mul(u64::from(data_samples)) + .ok_or(TiffError::LimitsExceeded)?; + let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?; + + let chunk_col_stride: usize = data_row_bits + .div_ceil(8) + .checked_mul(u64::from(tiff_chunk_dimensions.1)) + .ok_or(TiffError::LimitsExceeded)? + .try_into()?; + + let plane_stride: usize = data_row_bits + .div_ceil(8) + .checked_mul(u64::from(data_dimensions.1)) + .ok_or(TiffError::LimitsExceeded)? + .try_into()?; + + let minimum_row_stride = data_row_bytes; + + let chunks_across: u32 = data_dimensions.0.div_ceil(tiff_chunk_dimensions.0); + let chunks_per_plane = (self.chunk_offsets.len() as u32) / u32::from(strips_per_pixel); + + // We would not get an offset in byte units, sorry, no bit interleaving in the output. + if chunks_across > 1 && chunk_row_bits % 8 != 0 { + return Err(TiffError::UnsupportedError( + TiffUnsupportedError::MisalignedTileBoundaries, + )); + } + + Ok(ReadoutLayout { + planar_config: self.planar_config, + color, + minimum_row_stride, + sample_format: self.sample_format, + row_stride: data_row_bytes, + chunk_row_stride: chunk_row_bytes, + chunk_col_stride, + plane_stride, + tiff_bits_per_sample, + tiff_samples, + tiff_chunk_dimensions, + tiff_row_bytes, + chunks_across, + chunks_per_plane, + }) + } + pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> { let dims = self.chunk_dimensions()?; match self.chunk_type { ChunkType::Strip => { - let strip_attrs = self.strip_decoder.as_ref().unwrap(); - let strips_per_band = - self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1; + let rows_per_strip = dims.1; + let strips_per_band = self.height.div_ceil(rows_per_strip); + let strip_height_without_padding = (chunk_index % strips_per_band) .checked_mul(dims.1) .and_then(|x| self.height.checked_sub(x)) @@ -630,7 +711,7 @@ impl Image { &self, reader: &mut ValueReader, buf: &mut [u8], - output_row_stride: usize, + layout: &ReadoutLayout, chunk_index: u32, ) -> TiffResult<()> { let ValueReader { @@ -640,10 +721,10 @@ impl Image { } = reader; let byte_order = reader.byte_order; - let reader = reader.inner(); // Validate that the color type is supported. - let color_type = self.colortype()?; + let color_type = layout.color; + match color_type { ColorType::RGB(n) | ColorType::RGBA(n) @@ -714,8 +795,8 @@ impl Image { let photometric_interpretation = self.photometric_interpretation; let predictor = self.predictor; - let samples = self.samples_per_pixel(); - let data_samples = self.samples_per_out_texel(color_type); + let samples = layout.tiff_samples; + let data_samples = layout.samples_per_out_texel(); // We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this // takes up in the output. Each has an associated count of bits per pixel. The first @@ -726,31 +807,24 @@ impl Image { let chunk_dims = self.chunk_dimensions()?; let data_dims = self.chunk_data_dimensions(chunk_index)?; - let chunk_row_bits = (u64::from(chunk_dims.0) * u64::from(self.bits_per_sample)) - .checked_mul(samples as u64) - .ok_or(TiffError::LimitsExceeded)?; - let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?; - - let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample)) - .checked_mul(data_samples as u64) - .ok_or(TiffError::LimitsExceeded)?; - let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?; + let chunk_row_bytes: usize = layout.tiff_row_bytes; + let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?; // TODO: Should these return errors instead? - assert!(output_row_stride >= data_row_bytes); - assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes); + assert!(layout.minimum_row_stride >= data_row_bytes); + assert!(buf.len() >= layout.row_stride * (data_dims.1 as usize - 1) + data_row_bytes); + + let is_all_bits = samples == data_samples; + let is_output_chunk_rows = layout.row_stride == chunk_row_bytes; let mut reader = Self::create_reader( - reader, + reader.inner(), compression_method, *compressed_bytes, self.jpeg_tables.as_deref().map(|a| &**a), chunk_dims, )?; - let is_all_bits = samples == usize::from(data_samples); - let is_output_chunk_rows = output_row_stride == chunk_row_bytes; - if is_output_chunk_rows && is_all_bits { // Here we can read directly into the output buffer itself. let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize]; @@ -773,7 +847,7 @@ impl Image { // The floating point predictor shuffles the padding bytes into the encoded output, so // this case is handled specially when needed. let mut encoded = vec![0u8; chunk_row_bytes]; - for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) { + for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { reader.read_exact(&mut encoded)?; let row = &mut row[..data_row_bytes]; @@ -789,10 +863,14 @@ impl Image { } } else if is_all_bits { // We read row-by-row but each row fits in its output buffer. - for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) { + for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { let row = &mut row[..data_row_bytes]; - reader.read_exact(row)?; + let used = data_row_bytes.min(chunk_row_bytes); + // Two ways how we get here: we have more bytes in our chunk data than in the image + // we are to read. Then we need to skip the rest of the data. Or we have a bigger + // row stride than the chunk contains data, then we need to fill only the front. + reader.read_exact(&mut row[..used])?; // Skip horizontal padding if chunk_row_bytes > data_row_bytes { let len = u64::try_from(chunk_row_bytes - data_row_bytes)?; @@ -835,7 +913,7 @@ impl Image { let photo_range = photometric_bit_end / 8..bits_per_pixel / 8; let mut encoded = vec![0u8; chunk_row_bytes]; - for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) { + for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) { reader.read_exact(&mut encoded)?; Self::compact_photometric_bytes(&mut encoded, row, &photo_range); @@ -871,3 +949,118 @@ impl Image { }); } } + +impl ReadoutLayout { + pub(crate) fn samples_per_out_texel(&self) -> u16 { + match self.planar_config { + PlanarConfiguration::Chunky => self.color.num_samples(), + PlanarConfiguration::Planar => 1, + } + } + + // For a concrete chunk, which may be a partial border chunk, the byte length of one row of its + // pixel data. + pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult { + let data_samples = self.samples_per_out_texel(); + let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample)) + .checked_mul(u64::from(data_samples)) + .ok_or(TiffError::LimitsExceeded)?; + Ok(data_row_bits.div_ceil(8).try_into()?) + } + + pub(crate) fn set_row_stride(&mut self, row_stride: usize) -> Result<(), TiffError> { + if row_stride < self.minimum_row_stride { + return Err(TiffError::UsageError( + UsageError::InsufficientOutputRowStride { + needed: self.minimum_row_stride, + requested: row_stride, + }, + )); + } + + let data_row_bytes = u64::try_from(row_stride)?; + + let chunk_col_stride = data_row_bytes + .checked_mul(u64::from(self.tiff_chunk_dimensions.1)) + .ok_or(TiffError::LimitsExceeded)? + .try_into()?; + + let height = self.plane_stride.checked_div(self.row_stride); + + let plane_stride = height + .and_then(|h| data_row_bytes.checked_mul(h as u64)) + // If height was zero, or the previous stride was zero, there are no bytes in a plane + .unwrap_or(0) + .try_into()?; + + self.row_stride = row_stride; + self.chunk_col_stride = chunk_col_stride; + self.plane_stride = plane_stride; + + Ok(()) + } + + /// Reduce this down to the layout of the output planes. + pub(crate) fn to_plane_layout(&self) -> Result { + let num_planes = self.color.num_samples() / self.samples_per_out_texel(); + + // Using the standard range iterator as checked_add on steroids. + // + // Note: for supporting subsampling, adjust as required. + let mut offset = (0..=usize::MAX).step_by(self.plane_stride); + + let plane_offsets = offset.by_ref().take(usize::from(num_planes)).collect(); + + // Get the past-the-end of the last plane. + // + // This also verifies the `take` above was not short. + let Some(total_bytes) = offset.next() else { + return Err(TiffError::LimitsExceeded); + }; + + Ok(PlaneLayout { + plane_offsets, + total_bytes, + readout: self.clone(), + }) + } +} + +/// A `ReadoutLayout` with pre-calculated plane information. +pub(crate) struct PlaneLayout { + /// The underlying readout layout. + pub readout: ReadoutLayout, + /// Buffer offset from one plane of output to the next. + pub plane_offsets: Vec, + /// Total number of bytes for all planes in given order. + pub total_bytes: usize, +} + +impl PlaneLayout { + /// Return the number of planes to extract into the provided buffer. + pub(crate) fn used_planes(&self, buffer: &[impl Sized]) -> TiffResult { + self.readout.assert_min_layout(buffer)?; + let buffer_len = core::mem::size_of_val(buffer); + + // Note: with differently sized planes this is dependent on the plane. + let last_plane_start = buffer_len.checked_sub(self.readout.plane_stride); + + // Find how many planes fit into the output buffer. + let used_plane_offsets = self + .plane_offsets + .iter() + .enumerate() + // Find the first plane that would not fit completely at its offset. + .skip_while(|(_, &offset)| last_plane_start >= Some(offset)) + .nth(0) + // If all planes fit, use all of them. + .map_or(self.plane_offsets.len(), |(idx, _)| idx); + + debug_assert!( + used_plane_offsets <= usize::from(u16::MAX), + "Planes limited by number of samples, which is encoded as u16" + ); + + Ok(used_plane_offsets as u16) + } +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 7b42575..cc31572 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -22,6 +22,10 @@ mod image; mod stream; mod tag_reader; +/// An index referring to a (rectangular) region of an image. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TiffCodingUnit(pub u32); + /// Result of a decoding process #[derive(Debug)] pub enum DecodingResult { @@ -50,94 +54,87 @@ pub enum DecodingResult { } impl DecodingResult { - fn new_u8(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size { + /// Reallocate the buffer to decode all planes of the indicated layout. + pub fn resize_to( + &mut self, + buffer: &BufferLayoutPreference, + limits: &Limits, + ) -> Result<(), TiffError> { + let sample_type = buffer.sample_type.ok_or(TiffError::UnsupportedError( + TiffUnsupportedError::UnknownInterpretation, + ))?; + + let extent = sample_type.extent_for_bytes(buffer.complete_len); + self.resize_to_extent(extent, limits) + } + + fn resize_to_extent( + &mut self, + extent: DecodingExtent, + limits: &Limits, + ) -> Result<(), TiffError> { + // FIXME: we *can* reuse the allocation sometimes. + *self = extent.to_result_buffer(limits)?; + Ok(()) + } + + fn new( + size: usize, + limits: &Limits, + from_fn: fn(Vec) -> Self, + ) -> TiffResult { + if size > limits.decoding_buffer_size / core::mem::size_of::() { Err(TiffError::LimitsExceeded) } else { - Ok(DecodingResult::U8(vec![0; size])) + Ok(from_fn(vec![T::default(); size])) } } + fn new_u8(size: usize, limits: &Limits) -> TiffResult { + Self::new(size, limits, DecodingResult::U8) + } + fn new_u16(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 2 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::U16(vec![0; size])) - } + Self::new(size, limits, DecodingResult::U16) } fn new_u32(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 4 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::U32(vec![0; size])) - } + Self::new(size, limits, DecodingResult::U32) } fn new_u64(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 8 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::U64(vec![0; size])) - } + Self::new(size, limits, DecodingResult::U64) } fn new_f32(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / std::mem::size_of::() { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::F32(vec![0.0; size])) - } + Self::new(size, limits, DecodingResult::F32) } fn new_f64(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / std::mem::size_of::() { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::F64(vec![0.0; size])) - } + Self::new(size, limits, DecodingResult::F64) } fn new_f16(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / std::mem::size_of::() { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::F16(vec![f16::ZERO; size])) - } + Self::new(size, limits, DecodingResult::F16) } fn new_i8(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / std::mem::size_of::() { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::I8(vec![0; size])) - } + Self::new(size, limits, DecodingResult::I8) } fn new_i16(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 2 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::I16(vec![0; size])) - } + Self::new(size, limits, DecodingResult::I16) } fn new_i32(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 4 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::I32(vec![0; size])) - } + Self::new(size, limits, DecodingResult::I32) } fn new_i64(size: usize, limits: &Limits) -> TiffResult { - if size > limits.decoding_buffer_size / 8 { - Err(TiffError::LimitsExceeded) - } else { - Ok(DecodingResult::I64(vec![0; size])) - } + Self::new(size, limits, DecodingResult::I64) } + /// Get a view of this buffer starting from the nth _sample_ of the current type. pub fn as_buffer(&mut self, start: usize) -> DecodingBuffer<'_> { match *self { DecodingResult::U8(ref mut buf) => DecodingBuffer::U8(&mut buf[start..]), @@ -182,9 +179,25 @@ pub enum DecodingBuffer<'a> { } impl<'a> DecodingBuffer<'a> { - fn as_bytes_mut(&mut self) -> &mut [u8] { + pub fn as_bytes(&self) -> &[u8] { match self { - DecodingBuffer::U8(ref mut buf) => buf, + DecodingBuffer::U8(buf) => buf, + DecodingBuffer::I8(buf) => bytecast::i8_as_ne_bytes(buf), + DecodingBuffer::U16(buf) => bytecast::u16_as_ne_bytes(buf), + DecodingBuffer::I16(buf) => bytecast::i16_as_ne_bytes(buf), + DecodingBuffer::U32(buf) => bytecast::u32_as_ne_bytes(buf), + DecodingBuffer::I32(buf) => bytecast::i32_as_ne_bytes(buf), + DecodingBuffer::U64(buf) => bytecast::u64_as_ne_bytes(buf), + DecodingBuffer::I64(buf) => bytecast::i64_as_ne_bytes(buf), + DecodingBuffer::F16(buf) => bytecast::f16_as_ne_bytes(buf), + DecodingBuffer::F32(buf) => bytecast::f32_as_ne_bytes(buf), + DecodingBuffer::F64(buf) => bytecast::f64_as_ne_bytes(buf), + } + } + + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + match self { + DecodingBuffer::U8(buf) => buf, DecodingBuffer::I8(buf) => bytecast::i8_as_ne_mut_bytes(buf), DecodingBuffer::U16(buf) => bytecast::u16_as_ne_mut_bytes(buf), DecodingBuffer::I16(buf) => bytecast::i16_as_ne_mut_bytes(buf), @@ -197,6 +210,43 @@ impl<'a> DecodingBuffer<'a> { DecodingBuffer::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf), } } + + pub fn byte_len(&self) -> usize { + self.as_bytes().len() + } +} + +#[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. @@ -205,11 +255,131 @@ impl<'a> DecodingBuffer<'a> { /// caller provided buffer must fit the expectations of the decoder to be filled with data from the /// current image. #[non_exhaustive] +#[derive(Debug, Clone)] pub struct BufferLayoutPreference { /// Minimum byte size of the buffer to read image data. 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, /// Minimum number of bytes to represent a row of image data of the requested content. pub row_stride: Option, + /// Number of planes in the image. + pub planes: usize, + /// Number of bytes used to represent one plane. + pub plane_stride: Option, + /// Number of bytes of data when reading all planes. + pub complete_len: usize, +} + +impl BufferLayoutPreference { + fn from_planes(layout: &image::PlaneLayout) -> Self { + BufferLayoutPreference { + len: layout.readout.plane_stride, + row_stride: core::num::NonZeroUsize::new(layout.readout.row_stride), + planes: layout.plane_offsets.len(), + plane_stride: core::num::NonZeroUsize::new(layout.readout.plane_stride), + 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 { + 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 { + // 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, + ) -> TiffResult { + 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)] + fn assert_min_layout(&self, buffer: &[T]) -> TiffResult<()> { + if core::mem::size_of_val(buffer) < self.plane_stride { + Err(TiffError::UsageError( + UsageError::InsufficientOutputBufferSize { + needed: self.plane_stride, + provided: buffer.len(), + }, + )) + } else { + Ok(()) + } + } } /// The count and matching discriminant for a `DecodingBuffer`. @@ -266,17 +436,19 @@ 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(()) + 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, } } } @@ -367,7 +539,7 @@ pub struct IfdDecoder<'lt> { inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>, } -fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { +fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) { fn one_byte_predict(buf: &mut [u8]) { for i in N..buf.len() { buf[i] = buf[i].wrapping_add(buf[i - N]); @@ -390,6 +562,8 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { } } + let samples = usize::from(samples); + match (bit_depth, samples) { // Note we can't use `windows` or so due to the overlap between each iteration. We split // the cases by the samples / lookback constant so that each is optimized individually. @@ -456,7 +630,9 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) { } } -fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) { +fn predict_f32(input: &mut [u8], output: &mut [u8], samples: u16) { + let samples = usize::from(samples); + for i in samples..input.len() { input[i] = input[i].wrapping_add(input[i - samples]); } @@ -471,7 +647,9 @@ fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) { } } -fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) { +fn predict_f16(input: &mut [u8], output: &mut [u8], samples: u16) { + let samples = usize::from(samples); + for i in samples..input.len() { input[i] = input[i].wrapping_add(input[i - samples]); } @@ -484,7 +662,9 @@ fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) { } } -fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) { +fn predict_f64(input: &mut [u8], output: &mut [u8], samples: u16) { + let samples = usize::from(samples); + for i in samples..input.len() { input[i] = input[i].wrapping_add(input[i - samples]); } @@ -506,7 +686,7 @@ fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) { fn fix_endianness_and_predict( buf: &mut [u8], bit_depth: u8, - samples: usize, + samples: u16, byte_order: ByteOrder, predictor: Predictor, ) { @@ -977,112 +1157,21 @@ impl Decoder { Ok(u32::try_from(self.image().chunk_offsets.len())?) } - pub fn read_chunk_to_buffer( - &mut self, - mut buffer: DecodingBuffer, - chunk_index: u32, - output_width: usize, - ) -> TiffResult<()> { - let color = self.image.color_or_fallback(); - - let output_row_stride = (output_width as u64) - .saturating_mul(self.image.samples_per_out_texel(color) 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_row_stride: u64, + layout: &image::ReadoutLayout, ) -> TiffResult<()> { let offset = self.image.chunk_file_range(chunk_index)?.0; self.goto_offset_u64(offset)?; - self.image.expand_chunk( - &mut self.value_reader, - buffer, - output_row_stride.try_into()?, - chunk_index, - )?; + self.image + .expand_chunk(&mut self.value_reader, buffer, layout, chunk_index)?; Ok(()) } - fn result_buffer(&self, width: usize, height: usize) -> TiffResult { - self.result_extent(width, height)? - .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; - - // 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) - .div_ceil(8) - .try_into() - .map_err(|_| TiffError::LimitsExceeded)? - }; - - let color = self.image.color_or_fallback(); - let samples = self.image.samples_per_out_texel(color); - - let buffer_size = row_samples - .checked_mul(height) - .and_then(|x| x.checked_mul(samples.into())) - .ok_or(TiffError::LimitsExceeded)?; - - Ok(match self.image().sample_format { - 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), - n if n <= 64 => DecodingExtent::U64(buffer_size), - n => { - return Err(TiffError::UnsupportedError( - TiffUnsupportedError::UnsupportedBitsPerChannel(n), - )) - } - }, - SampleFormat::IEEEFP => match bits_per_sample { - 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 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), - n if n <= 64 => DecodingExtent::I64(buffer_size), - 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 without being specific as to the underlying type for forward @@ -1090,41 +1179,78 @@ 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, ) -> TiffResult { let data_dims = self.image().chunk_data_dimensions(chunk_index)?; - let layout = self - .result_extent(data_dims.0 as usize, data_dims.1 as usize)? - .preferred_layout()?; + let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?; - let row_stride = self.image.minimum_row_stride(data_dims); + 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 plane_stride = core::num::NonZeroUsize::new(readout.plane_stride); Ok(BufferLayoutPreference { len: layout.size(), row_stride, + planes: 1, + plane_stride, + complete_len: layout.size(), + sample_format: self.image().sample_format, + sample_type: Some(sample_type), }) } + /// Return the layout preferred to read several planes corresponding to the specified region. + /// + /// This is similar to [`Self::image_chunk_buffer_layout`] but can read chunks from all planes + /// at the corresponding coordinates of the image. + /// + /// # Bugs + /// + /// Sub-sampled images are not yet supported properly. + pub fn image_coding_unit_layout( + &mut self, + code_unit: TiffCodingUnit, + ) -> TiffResult { + match self.image().planar_config { + PlanarConfiguration::Chunky => return self.image_chunk_buffer_layout(code_unit.0), + PlanarConfiguration::Planar => {} + } + + let (width, height) = self.image().chunk_data_dimensions(code_unit.0)?; + + let layout = self + .image() + .readout_for_size(width, height)? + .to_plane_layout()?; + + if code_unit.0 >= layout.readout.chunks_per_plane { + return Err(TiffError::UsageError(UsageError::InvalidCodingUnit( + code_unit.0, + layout.readout.chunks_per_plane, + ))); + } + + Ok(BufferLayoutPreference::from_planes(&layout)) + } + /// 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`. + /// Note that for planar images each chunk contains only one sample of the underlying data. pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult { - let data_dims = self.image().chunk_data_dimensions(chunk_index)?; + let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; - let mut result = self.result_buffer(data_dims.0 as usize, data_dims.1 as usize)?; + let readout = self.image().readout_for_size(width, height)?; - self.read_chunk_to_buffer(result.as_buffer(0), chunk_index, data_dims.0 as usize)?; + let mut result = readout + .result_extent_for_planes(0..1)? + .to_result_buffer(&self.value_reader.limits)?; + + self.read_chunk_to_bytes(result.as_buffer(0).as_bytes_mut(), chunk_index, &readout)?; Ok(result) } @@ -1135,22 +1261,95 @@ impl Decoder { /// 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. /// + /// Note that for planar images each chunk contains only one sample of the underlying data. + pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> { + let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; + + let layout = self.image().readout_for_size(width, height)?; + layout.assert_min_layout(buffer)?; + + self.read_chunk_to_bytes(buffer, chunk_index, &layout)?; + + Ok(()) + } + + /// Read the specified chunk (at index `chunk_index`) into a provide buffer. + /// + /// It will re-allocate the buffer into the correct type and size, within the decoder's + /// configured limits, and then pass it to the underlying method. This is essentially a + /// type-safe wrapper around the raw [`Self::read_chunk_to_bytes`] method. + /// + /// Note that for planar images each chunk contains only one sample of the underlying data. + pub fn read_chunk_to_buffer( + &mut self, + buffer: &mut DecodingResult, + chunk_index: u32, + output_width: usize, + ) -> TiffResult<()> { + let (width, height) = self.image().chunk_data_dimensions(chunk_index)?; + + let mut layout = self.image().readout_for_size(width, height)?; + layout.set_row_stride(output_width)?; + + let extent = layout.result_extent_for_planes(0..1)?; + buffer.resize_to_extent(extent, &self.value_reader.limits)?; + + self.read_chunk_to_bytes(buffer.as_buffer(0).as_bytes_mut(), chunk_index, &layout)?; + + Ok(()) + } + + /// Read chunks corresponding to several planes of a region of pixels. + /// + /// For non planar images this is equivalent to [`Self::read_chunk_bytes`] as there is only one + /// plane in the image. For planar images the planes are stored consecutively into the output + /// buffer. Returns an error if not enough space for at least one plane is provided. Otherwise + /// reads all planes that can be stored completely in the provided output buffer. + /// + /// A region is a rectangular assortment of pixels in the image, depending on the chunk type + /// either strips or tiles. Borrowing terminology from JPEG we call the collection of all + /// chunks from all planes that encode samples from the same region a "coding unit". + /// /// # 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)?; + /// Sub-sampled images are not yet supported properly. + pub fn read_coding_unit_bytes( + &mut self, + slice: TiffCodingUnit, + buffer: &mut [u8], + ) -> TiffResult<()> { + let (width, height) = self.image().chunk_data_dimensions(slice.0)?; + let readout = self.image().readout_for_size(width, height)?; - let extent = self.result_extent(data_dims.0 as usize, data_dims.1 as usize)?; - extent.assert_layout(buffer.len())?; + let ref layout @ image::PlaneLayout { + ref plane_offsets, + // We assume that is correct, so really it can be ignored. + total_bytes: _, + ref readout, + } = readout.to_plane_layout()?; - 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); + if slice.0 >= readout.chunks_per_plane { + return Err(TiffError::UsageError(UsageError::InvalidCodingUnit( + slice.0, + readout.chunks_per_plane, + ))); + } - self.read_chunk_to_bytes(buffer, chunk_index, output_row_stride)?; + // No subsamples planes support, for now. + let used_plane_offsets = usize::from(layout.used_planes(buffer)?); + debug_assert!(used_plane_offsets >= 1, "Should have errored"); + + for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() { + let chunk = slice.0 + idx as u32 * readout.chunks_per_plane; + self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?; + + self.image.expand_chunk( + &mut self.value_reader, + &mut buffer[plane_offset..], + readout, + chunk, + )?; + } Ok(()) } @@ -1183,114 +1382,121 @@ impl Decoder { /// 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); - - 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, - }) + let layout = self.image().readout_for_image()?.to_plane_layout()?; + Ok(BufferLayoutPreference::from_planes(&layout)) } /// Decodes the entire image and return it as a Vector /// + /// # Examples + /// + /// This method is deprecated. For replacement usage see `examples/decode.rs`. + /// /// # 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`. + /// + /// # Intent to deprecate + /// + /// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving + /// the buffer across multiple calls. This old method will likely keep its bugged planar + /// behavior until it is fully replaced, to ensure that existing code will not run into + /// unexpectedly large allocations that will error on limits instead. pub fn read_image(&mut self) -> TiffResult { - let width = self.image().width; - let height = self.image().height; + let readout = self.image().readout_for_image()?; + + let mut result = readout + .result_extent_for_planes(0..1)? + .to_result_buffer(&self.value_reader.limits)?; - 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) } + /// Decodes the entire image into a provided buffer. + /// + /// It will re-allocate the buffer into the correct type and size, within the decoder's + /// configured limits, and then pass it to the underlying method. This is essentially a + /// type-safe wrapper around the raw [`Self::read_image_bytes`] method. + /// + /// # Examples + /// + /// ``` + /// use tiff::decoder::{Decoder, DecodingResult, Limits}; + /// + /// let mut result = DecodingResult::I8(vec![]); + /// + /// let mut reader = /* */ + /// # Decoder::new(std::io::Cursor::new(include_bytes!(concat!( + /// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif" + /// # )))).unwrap(); + /// + /// reader.read_image_to_buffer(&mut result)?; + /// + /// # Ok::<_, tiff::TiffError>(()) + /// ``` + pub fn read_image_to_buffer( + &mut self, + result: &mut DecodingResult, + ) -> TiffResult { + let readout = self.image().readout_for_image()?; + + let planes = readout.to_plane_layout()?; + let layout = BufferLayoutPreference::from_planes(&planes); + + let extent = readout.result_extent_for_planes(0..1)?; + result.resize_to_extent(extent, &self.value_reader.limits)?; + self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?; + + Ok(layout) + } + /// 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. /// - /// # Bugs + /// # Error /// - /// 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`. + /// Returns an error if the buffer fits less than one plane. In particular, for non-planar + /// images returns an error if the buffer does not fit the required size. pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { - let width = self.image().width; - let height = self.image().height; + let readout = self.image().readout_for_image()?; - let extent = self.result_extent(width as usize, height as usize)?; - extent.assert_layout(buffer.len())?; + let ref layout @ image::PlaneLayout { + ref plane_offsets, + // We assume that is correct, so really it can be ignored. + total_bytes: _, + ref readout, + } = readout.to_plane_layout()?; - if width == 0 || height == 0 { - return Ok(()); - } + let used_plane_offsets = usize::from(layout.used_planes(buffer)?); + debug_assert!(used_plane_offsets >= 1, "Should have errored"); - let chunk_dimensions = self.image().chunk_dimensions()?; - let chunk_dimensions = ( - chunk_dimensions.0.min(width), - chunk_dimensions.1.min(height), - ); - - if chunk_dimensions.0 == 0 || chunk_dimensions.1 == 0 { - return Err(TiffError::FormatError( - TiffFormatError::InconsistentSizesEncountered, - )); - } - - let color = self.image().colortype()?; - let samples = self.image().samples_per_out_texel(color); - - if samples == 0 { - return Err(TiffError::FormatError( - TiffFormatError::InconsistentSizesEncountered, - )); - } - - let output_row_bits = (width as u64 * self.image.bits_per_sample as u64) - .checked_mul(samples as u64) - .ok_or(TiffError::LimitsExceeded)?; - let output_row_stride: usize = output_row_bits.div_ceil(8).try_into()?; - - let chunk_row_bits = (chunk_dimensions.0 as u64 * self.image.bits_per_sample as u64) - .checked_mul(samples as u64) - .ok_or(TiffError::LimitsExceeded)?; - let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?; - - let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize; - - if chunks_across > 1 && chunk_row_bits % 8 != 0 { - return Err(TiffError::UnsupportedError( - TiffUnsupportedError::MisalignedTileBoundaries, - )); - } - - let image_chunks = self.image().chunk_offsets.len() / self.image().strips_per_pixel(); // For multi-band images, only the first band is read. // Possible improvements: // * pass requested band as parameter // * collect bands to a RGB encoding result in case of RGB bands - for chunk in 0..image_chunks { - self.goto_offset_u64(self.image().chunk_offsets[chunk])?; + for chunk in 0..readout.chunks_per_plane { + let x = (chunk % readout.chunks_across) as usize; + let y = (chunk / readout.chunks_across) as usize; - let x = chunk % chunks_across; - let y = chunk / chunks_across; - let buffer_offset = - y * output_row_stride * chunk_dimensions.1 as usize + x * chunk_row_bytes; - self.image.expand_chunk( - &mut self.value_reader, - &mut buffer[buffer_offset..], - output_row_stride, - chunk as u32, - )?; + let buffer_offset = y * readout.chunk_col_stride + x * readout.chunk_row_stride; + + for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() { + let chunk = chunk + idx as u32 * readout.chunks_per_plane; + self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?; + + self.image.expand_chunk( + &mut self.value_reader, + &mut buffer[plane_offset..][buffer_offset..], + readout, + chunk, + )?; + } } Ok(()) diff --git a/src/encoder/writer.rs b/src/encoder/writer.rs index e3585cc..6b7eb16 100644 --- a/src/encoder/writer.rs +++ b/src/encoder/writer.rs @@ -221,6 +221,8 @@ fn encoder_on_short_writes() { }; let imgdata = vec![42u8; 100 * 100]; + let mut data = crate::decoder::DecodingResult::U8(vec![]); + for comp in [ super::Compression::Uncompressed, super::Compression::Packbits, @@ -240,8 +242,12 @@ fn encoder_on_short_writes() { write.inner.set_position(0); let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap(); - let data = tiff_reader.read_image().unwrap(); + let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap(); - assert!(matches!(data, crate::decoder::DecodingResult::U8(v) if v == imgdata)); + assert!( + matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata), + "{data:?} @ {}", + data.as_buffer(0).byte_len() + ); } } diff --git a/src/error.rs b/src/error.rs index e8977e2..e0c06cb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -175,6 +175,12 @@ quick_error! { InvalidChunkIndex(index: u32) { display("invalid chunk index ({index}) requested") } + InvalidPlaneIndex(index: u16) { + display("invalid plane index ({index}) requested") + } + InvalidCodingUnit(index: u32, have: u32) { + display("out of bounds coding unit ({index}) requested, only {have} available") + } PredictorCompressionMismatch { display("requested predictor is not compatible with the requested compression") } @@ -187,6 +193,9 @@ quick_error! { InsufficientOutputBufferSize { needed: usize, provided: usize } { display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}") } + InsufficientOutputRowStride { needed: usize, requested: usize } { + display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}") + } ZeroIfdPointer { display("the offset 0 can not point to a valid IFD") } diff --git a/tests/decode_planes.rs b/tests/decode_planes.rs new file mode 100644 index 0000000..ea9770c --- /dev/null +++ b/tests/decode_planes.rs @@ -0,0 +1,76 @@ +use tiff::decoder::{Decoder, TiffCodingUnit}; + +use std::fs::File; +use std::path::PathBuf; + +const TEST_IMAGE_DIR: &str = "./tests/images/"; + +#[test] +fn read_all_planes() { + let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif"); + let file = File::open(path).unwrap(); + let io = std::io::BufReader::new(file); + + let mut tif = Decoder::new(io).unwrap(); + let layout = tif.image_buffer_layout().unwrap(); + assert_eq!(layout.planes, 3); + let plane_stride = layout.plane_stride.map_or(0, |x| x.get()); + + eprintln!("{:?}", layout); + let mut buffer = vec![0u8; layout.complete_len]; + tif.read_image_bytes(&mut buffer).unwrap(); + + // Mainly you can see: these are different and non-zero. Otherwise these magic constants just + // depend on the file itself that is being used. + const PLANE_EXCPECTED_SUMS: [u32; 3] = [15417630, 13007788, 11103530]; + + for (plane, expected) in (0..layout.planes).zip(PLANE_EXCPECTED_SUMS) { + let plane_data = &buffer[plane_stride * plane..][..layout.len]; + let sum: u32 = plane_data.iter().copied().map(u32::from).sum(); + assert_eq!(sum, expected); + } +} + +#[test] +fn read_first_coding_unit() { + let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif"); + let file = File::open(path).unwrap(); + let io = std::io::BufReader::new(file); + + let mut tif = Decoder::new(io).unwrap(); + let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap(); + + assert_eq!(layout.planes, 3); + let plane_stride = layout.plane_stride.map_or(0, |x| x.get()); + eprintln!("{:?}", layout); + let mut buffer = vec![0u8; layout.complete_len]; + + struct Expected { + plane_sums: [u32; 3], + unit: TiffCodingUnit, + } + + // Mainly you can see: these are different and non-zero. Otherwise these magic constants just + // depend on the file itself that is being used. Also this varies by what unit we are reading. + const PLANE_EXCPECTED_SUMS: [Expected; 2] = [ + Expected { + plane_sums: [856853, 767686, 688532], + unit: TiffCodingUnit(0), + }, + Expected { + plane_sums: [883679, 797329, 711086], + unit: TiffCodingUnit(1), + }, + ]; + + for expected in PLANE_EXCPECTED_SUMS { + tif.read_coding_unit_bytes(expected.unit, &mut buffer) + .unwrap(); + + for (plane, expected) in (0..layout.planes).zip(expected.plane_sums) { + let plane_data = &buffer[plane_stride * plane..][..layout.len]; + let sum: u32 = plane_data.iter().copied().map(u32::from).sum(); + assert_eq!(sum, expected); + } + } +} diff --git a/tests/encode_images.rs b/tests/encode_images.rs index b16e87d..df0dac9 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -81,7 +81,10 @@ fn encode_decode() { decoder.get_tag(Tag::Artist).unwrap(), ifd::Value::Ascii("Image-tiff".into()) ); - if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() { + + let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); + let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let tiff::decoder::DecodingResult::U8(img_res) = buffer { assert_eq!(image_data, img_res); } else { panic!("Wrong data type"); @@ -161,7 +164,9 @@ fn encode_decode_big() { decoder.get_tag(Tag::Artist).unwrap(), ifd::Value::Ascii("Image-tiff".into()) ); - if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() { + let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); + let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::U8(img_res) = buffer { assert_eq!(image_data, img_res); } else { panic!("Wrong data type"); @@ -245,7 +250,10 @@ macro_rules! test_roundtrip { let mut decoder = Decoder::new(img_file).expect("Cannot create decoder"); assert_eq!(decoder.colortype().unwrap(), expected_type); - let image_data = match decoder.read_image().unwrap() { + let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + + let image_data = match buffer { DecodingResult::$buffer(res) => res, _ => panic!("Wrong data type"), }; @@ -259,9 +267,11 @@ macro_rules! test_roundtrip { } file.seek(SeekFrom::Start(0)).unwrap(); { + let mut buffer = tiff::decoder::DecodingResult::U8(vec![]); let mut decoder = Decoder::new(&mut file).unwrap(); - if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() { - assert_eq!(image_data, img_res); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::$buffer(img_res) = buffer { + assert_eq!(*image_data, img_res); } else { panic!("Wrong data type"); }