Add method to retrieve planes of a tile

This is well-defined even if we add sub-sampling support later on. We
can adjust the layout type accordingly with factors per each plane. Note
that the layout, in theory, does not depend on the index of the unit if
we describe all planes for a multi-planar layout. The specification
requires that the subsampling factors divide the tile sizes. However we
have to handle non-planar images here and this leaves us open to other
potential parameters.
This commit is contained in:
A. Molzer committed 2025-12-28 15:06:10 +01:00
1 parent 5b9dc30919
commit 1f02ff8ca1
4 files changed
+181 -26

No files matched your search

+39 -10
View File
@@ -123,16 +123,6 @@ pub(crate) struct ReadoutLayout {
pub chunks_per_plane: u32, 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<usize>,
/// Total number of bytes for all planes in given order.
pub total_bytes: usize,
}
impl Image { impl Image {
pub fn from_reader<R: Read + Seek>( pub fn from_reader<R: Read + Seek>(
decoder: &mut ValueReader<R>, decoder: &mut ValueReader<R>,
@@ -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<usize>,
/// 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<u16> {
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)
}
}
+94 -15
View File
@@ -22,6 +22,10 @@ mod image;
mod stream; mod stream;
mod tag_reader; 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 /// Result of a decoding process
#[derive(Debug)] #[derive(Debug)]
pub enum DecodingResult { pub enum DecodingResult {
@@ -1110,6 +1114,40 @@ impl<R: Read + Seek> Decoder<R> {
}) })
} }
/// 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<BufferLayoutPreference> {
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. /// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector.
/// ///
/// # Bugs /// # Bugs
@@ -1152,6 +1190,61 @@ impl<R: Read + Seek> Decoder<R> {
Ok(()) 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 /// 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`. /// the value returned here. For the size of the data (chunk minus padding), use `chunk_data_dimensions`.
pub fn chunk_dimensions(&self) -> (u32, u32) { pub fn chunk_dimensions(&self) -> (u32, u32) {
@@ -1227,21 +1320,7 @@ impl<R: Read + Seek> Decoder<R> {
ref readout, ref readout,
} = readout.to_plane_layout()?; } = readout.to_plane_layout()?;
layout.readout.assert_min_layout(buffer)?; let used_plane_offsets = usize::from(layout.used_planes(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);
debug_assert!(used_plane_offsets >= 1, "Should have errored"); debug_assert!(used_plane_offsets >= 1, "Should have errored");
// For multi-band images, only the first band is read. // For multi-band images, only the first band is read.
+3
View File
@@ -178,6 +178,9 @@ quick_error! {
InvalidPlaneIndex(index: u16) { InvalidPlaneIndex(index: u16) {
display("invalid plane index ({index}) requested") display("invalid plane index ({index}) requested")
} }
InvalidCodingUnit(index: u32, have: u32) {
display("out of bounds coding unit ({index}) requested, only {have} available")
}
PredictorCompressionMismatch { PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression") display("requested predictor is not compatible with the requested compression")
} }
+45 -1
View File
@@ -1,4 +1,4 @@
use tiff::decoder::Decoder; use tiff::decoder::{Decoder, TiffCodingUnit};
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
@@ -30,3 +30,47 @@ fn read_all_planes() {
assert_eq!(sum, expected); 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);
}
}
}