diff --git a/src/decoder/image.rs b/src/decoder/image.rs index 849550b..1845f80 100644 --- a/src/decoder/image.rs +++ b/src/decoder/image.rs @@ -123,16 +123,6 @@ pub(crate) struct ReadoutLayout { pub chunks_per_plane: u32, } -/// 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 Image { pub fn from_reader( decoder: &mut ValueReader, @@ -1032,3 +1022,42 @@ impl ReadoutLayout { }) } } + +/// 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 91d2592..2c4a8ac 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 { @@ -1110,6 +1114,40 @@ impl Decoder { }) } + /// 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 @@ -1152,6 +1190,61 @@ impl Decoder { 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 + /// + /// 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 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 slice.0 >= readout.chunks_per_plane { + return Err(TiffError::UsageError(UsageError::InvalidCodingUnit( + slice.0, + readout.chunks_per_plane, + ))); + } + + // 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(()) + } + /// 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) { @@ -1227,21 +1320,7 @@ impl Decoder { ref readout, } = readout.to_plane_layout()?; - layout.readout.assert_min_layout(buffer)?; - - // Note: with differently sized planes this is dependent on the plane. - let last_plane_start = buffer.len().checked_sub(readout.plane_stride); - - // Find how many planes fit into the output buffer. - let used_plane_offsets = 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(plane_offsets.len(), |(idx, _)| idx); - + let used_plane_offsets = usize::from(layout.used_planes(buffer)?); debug_assert!(used_plane_offsets >= 1, "Should have errored"); // For multi-band images, only the first band is read. diff --git a/src/error.rs b/src/error.rs index 25de149..e0c06cb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -178,6 +178,9 @@ quick_error! { 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") } diff --git a/tests/decode_planes.rs b/tests/decode_planes.rs index 5b221da..ea9770c 100644 --- a/tests/decode_planes.rs +++ b/tests/decode_planes.rs @@ -1,4 +1,4 @@ -use tiff::decoder::Decoder; +use tiff::decoder::{Decoder, TiffCodingUnit}; use std::fs::File; use std::path::PathBuf; @@ -30,3 +30,47 @@ fn read_all_planes() { 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); + } + } +}