Compare commits

..
Author SHA1 Message Date
A. Molzer cad95f255e Retain LZW decoder across tiles 2026-08-10 22:15:59 +02:00
A. Molzer 988a5a0c07 Make it an extension trait of Read 2026-08-10 21:09:32 +02:00
A. Molzer 5942111bb5 Add ReadSamples trait for decompression reading 2026-08-10 21:08:58 +02:00
12 changed files with 296 additions and 144 deletions

No files matched your search

+1 -2
View File
@@ -1,6 +1,5 @@
/target/ /target/
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
tests/benches/corpus/ tests/benches/corpus/
.idea
.agentbridge
+3 -5
View File
@@ -1,7 +1,8 @@
[package] [package]
name = "ndbioimage-tiff" name = "tiff"
version = "0.11.3" version = "0.11.3"
edition = "2021" edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml` # note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.88.0" rust-version = "1.88.0"
@@ -15,9 +16,6 @@ categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"] exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[lib]
name = "tiff"
[dependencies] [dependencies]
half = { version = "2.4.1" } half = { version = "2.4.1" }
quick-error = "2.0.1" quick-error = "2.0.1"
@@ -26,7 +24,7 @@ quick-error = "2.0.1"
fax34 = { package = "fax", version = "0.2.6", optional = true } fax34 = { package = "fax", version = "0.2.6", optional = true }
flate2 = { version = "1.0.20", optional = true } flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.2.1", optional = true } weezl = { version = "0.2.1", optional = true }
zstd = { version = "0.14", optional = true } zstd = { version = "0.13", optional = true }
zrip-decode = { version = "0.8.2", features = ["paranoid"], optional = true } zrip-decode = { version = "0.8.2", features = ["paranoid"], optional = true }
zune-jpeg = { version = "0.5.1", optional = true } zune-jpeg = { version = "0.5.1", optional = true }
image-webp = {version = "0.2.4", optional = true } image-webp = {version = "0.2.4", optional = true }
-2
View File
@@ -5,8 +5,6 @@
TIFF decoding and encoding library in pure Rust TIFF decoding and encoding library in pure Rust
Forked from https://github.com/image-rs/image-tiff. Changes: read images containing RGBPalette frames, ignoring colormaps.
## Supported ## Supported
### Features ### Features
+22 -42
View File
@@ -599,10 +599,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<2>() .chunks_exact(2)
.0 .map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Short(u16::from_ne_bytes(ch))),
) )
}) })
} }
@@ -611,10 +609,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<2>() .chunks_exact(2)
.0 .map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| SignedShort(i16::from_ne_bytes(ch))),
) )
}) })
} }
@@ -623,10 +619,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<4>() .chunks_exact(4)
.0 .map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Unsigned(u32::from_ne_bytes(ch))),
) )
}) })
} }
@@ -635,10 +629,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<4>() .chunks_exact(4)
.0 .map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Signed(i32::from_ne_bytes(ch))),
) )
}) })
} }
@@ -647,10 +639,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<4>() .chunks_exact(4)
.0 .map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Float(f32::from_ne_bytes(ch))),
) )
}) })
} }
@@ -659,17 +649,15 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<8>() .chunks_exact(8)
.0 .map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Double(f64::from_ne_bytes(ch))),
) )
}) })
} }
Type::RATIONAL => { Type::RATIONAL => {
v = Self::vec_with_capacity(self.count, self.type_, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| { v.extend(bytes.chunks_exact(8).map(|ch| {
Rational( Rational(
u32::from_ne_bytes(ch[..4].try_into().unwrap()), u32::from_ne_bytes(ch[..4].try_into().unwrap()),
u32::from_ne_bytes(ch[4..].try_into().unwrap()), u32::from_ne_bytes(ch[4..].try_into().unwrap()),
@@ -680,7 +668,7 @@ impl Entry {
Type::SRATIONAL => { Type::SRATIONAL => {
v = Self::vec_with_capacity(self.count, self.type_, limits)?; v = Self::vec_with_capacity(self.count, self.type_, limits)?;
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend(bytes.as_chunks::<8>().0.iter().map(|ch| { v.extend(bytes.chunks_exact(8).map(|ch| {
SRational( SRational(
i32::from_ne_bytes(ch[..4].try_into().unwrap()), i32::from_ne_bytes(ch[..4].try_into().unwrap()),
i32::from_ne_bytes(ch[4..].try_into().unwrap()), i32::from_ne_bytes(ch[4..].try_into().unwrap()),
@@ -693,10 +681,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<8>() .chunks_exact(8)
.0 .map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| UnsignedBig(u64::from_ne_bytes(ch))),
) )
}) })
} }
@@ -705,10 +691,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<8>() .chunks_exact(8)
.0 .map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| SignedBig(i64::from_ne_bytes(ch))),
) )
}) })
} }
@@ -717,10 +701,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<4>() .chunks_exact(4)
.0 .map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| Ifd(u32::from_ne_bytes(ch))),
) )
}) })
} }
@@ -729,10 +711,8 @@ impl Entry {
self.decode_values(self.count, self.type_, reader, |bytes| { self.decode_values(self.count, self.type_, reader, |bytes| {
v.extend( v.extend(
bytes bytes
.as_chunks::<8>() .chunks_exact(8)
.0 .map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
.iter()
.map(|&ch| IfdBig(u64::from_ne_bytes(ch))),
) )
}) })
} }
+21 -25
View File
@@ -1,8 +1,9 @@
use super::ifd::Value; use super::ifd::Value;
use super::stream::PackBitsReader; use super::stream::{PackBitsReader, ReadSamples};
use super::tag_reader::TagReader; use super::tag_reader::TagReader;
use super::ChunkType; use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader}; use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::decoder::stream::CompressionEngines;
use crate::tags::{ use crate::tags::{
ByteOrder, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, ByteOrder, CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration,
Predictor, SampleFormat, Tag, Predictor, SampleFormat, Tag,
@@ -11,7 +12,7 @@ use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
}; };
use std::io::{self, Cursor, Read, Seek}; use std::io::{Cursor, Read, Seek};
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug)] #[derive(Debug)]
@@ -36,9 +37,6 @@ impl TileAttributes {
pub fn tiles_down(&self) -> usize { pub fn tiles_down(&self) -> usize {
self.image_height.div_ceil(self.tile_length) self.image_height.div_ceil(self.tile_length)
} }
pub fn tiles_per_plane(&self) -> usize {
self.tiles_across() * self.tiles_down()
}
fn padding_right(&self) -> usize { fn padding_right(&self) -> usize {
(self.tile_width - self.image_width % self.tile_width) % self.tile_width (self.tile_width - self.image_width % self.tile_width) % self.tile_width
} }
@@ -179,7 +177,7 @@ impl Image {
.map(Value::into_u16) .map(Value::into_u16)
.transpose()? .transpose()?
.and_then(PhotometricInterpretation::from_u16) .and_then(PhotometricInterpretation::from_u16)
.unwrap_or(PhotometricInterpretation::BlackIsZero); .ok_or(TiffUnsupportedError::UnknownInterpretation)?;
// Try to parse both the compression method and the number, format, and bits of the included samples. // Try to parse both the compression method and the number, format, and bits of the included samples.
// If they are not explicitly specified, those tags are reset to their default values and not carried from previous images. // If they are not explicitly specified, those tags are reset to their default values and not carried from previous images.
@@ -470,11 +468,11 @@ impl Image {
} }
Some(map) Some(map)
} }
// A missing palette is not an error. The pixel values of an indexed image are just None => {
// indices into the palette, and the palette itself is only display information, so return Err(TiffError::FormatError(
// the image still decodes. Callers observe the absence as `None` from TiffFormatError::RequiredTagNotFound(Tag::ColorMap),
// `Decoder::color_map`. ));
None => None, }
} }
} else { } else {
None None
@@ -701,7 +699,8 @@ impl Image {
dimensions: (u32, u32), dimensions: (u32, u32),
samples: u16, samples: u16,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16, #[cfg_attr(not(feature = "fax"), allow(unused_variables))] fill_order: u16,
) -> TiffResult<Box<dyn Read + 'r>> { engines: &mut CompressionEngines,
) -> TiffResult<Box<dyn ReadSamples + 'r>> {
Ok(match compression_method { Ok(match compression_method {
CompressionMethod::None => Box::new(reader), CompressionMethod::None => Box::new(reader),
CompressionMethod::SgiLog => match samples { CompressionMethod::SgiLog => match samples {
@@ -732,6 +731,7 @@ impl Image {
CompressionMethod::LZW => Box::new(super::stream::LZWReader::new( CompressionMethod::LZW => Box::new(super::stream::LZWReader::new(
reader, reader,
usize::try_from(compressed_length)?, usize::try_from(compressed_length)?,
engines,
)), )),
#[cfg(feature = "zstd")] #[cfg(feature = "zstd")]
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?), CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
@@ -1019,15 +1019,7 @@ impl Image {
} }
ChunkType::Tile => { ChunkType::Tile => {
let tile_attrs = self.tile_attributes.as_ref().unwrap(); let tile_attrs = self.tile_attributes.as_ref().unwrap();
// The chunk index counts the tiles of all planes, but the tile grid repeats per let (padding_right, padding_down) = tile_attrs.get_padding(chunk_index as usize);
// plane, so only the position within the plane decides the padding.
let tiles_per_plane = tile_attrs.tiles_per_plane();
let tile = if tiles_per_plane == 0 {
0
} else {
chunk_index as usize % tiles_per_plane
};
let (padding_right, padding_down) = tile_attrs.get_padding(tile);
let tile_width = tile_attrs.tile_width - padding_right; let tile_width = tile_attrs.tile_width - padding_right;
let tile_length = tile_attrs.tile_length - padding_down; let tile_length = tile_attrs.tile_length - padding_down;
@@ -1044,6 +1036,7 @@ impl Image {
layout: &ReadoutLayout, layout: &ReadoutLayout,
chunk_index: u32, chunk_index: u32,
scratch: &mut Vec<u8>, scratch: &mut Vec<u8>,
engines: &mut CompressionEngines,
) -> TiffResult<()> { ) -> TiffResult<()> {
let ValueReader { let ValueReader {
reader, reader,
@@ -1190,6 +1183,7 @@ impl Image {
chunk_dims, chunk_dims,
self.samples, self.samples,
self.fill_order, self.fill_order,
engines,
)?; )?;
// Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to // Extended bit depths (9-15, 17-31): packed N-bit samples must be unpacked to
@@ -1319,11 +1313,12 @@ impl Image {
// Two ways how we get here: we have more bytes in our chunk data than in the image // 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 // 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. // 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 { if chunk_row_bytes > data_row_bytes {
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?; // Skip horizontal padding
io::copy(&mut reader.by_ref().take(len), &mut io::sink())?; let skip = u64::try_from(chunk_row_bytes - data_row_bytes)?;
reader.read_padded_samples(&mut row[..used], skip)?;
} else {
reader.read_exact(&mut row[..used])?;
} }
super::fix_endianness_and_predict( super::fix_endianness_and_predict(
@@ -1413,6 +1408,7 @@ impl Image {
} }
} }
reader.cache_or_destroy(engines);
Ok(()) Ok(())
} }
+24
View File
@@ -83,6 +83,14 @@ impl<R: Read> Read for LogLuv24<R> {
} }
} }
impl<R: Read> super::stream::ReadSamples for LogLuv24<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
fn packbits_decode<R: Read, const N: usize>( fn packbits_decode<R: Read, const N: usize>(
reader: &mut R, reader: &mut R,
buf: &mut [[u8; N]], buf: &mut [[u8; N]],
@@ -212,6 +220,14 @@ impl<R: Read> Read for LogLuv32<R> {
} }
} }
impl<R: Read> super::stream::ReadSamples for LogLuv32<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Pack-Bits like encoding for a single 16-bit log-L sample. /// Pack-Bits like encoding for a single 16-bit log-L sample.
pub struct LogLuv16<R> { pub struct LogLuv16<R> {
reader: Take<R>, reader: Take<R>,
@@ -278,6 +294,14 @@ impl<R: Read> Read for LogLuv16<R> {
} }
} }
impl<R: Read> super::stream::ReadSamples for LogLuv16<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> std::io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
// Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI // Table information from: `libtiff`, 1997 by Greg Ward Larson, SGI
#[doc(alias = "UV_SQSIZ")] #[doc(alias = "UV_SQSIZ")]
const UV_QUANTIZATION_WIDTH: f32 = 0.003500; const UV_QUANTIZATION_WIDTH: f32 = 0.003500;
+21 -9
View File
@@ -259,6 +259,7 @@ pub struct Decoder<R> {
scratch: Vec<u8>, scratch: Vec<u8>,
/// Leniency flag that allows libtiff-like TIFF color interpretation behavior /// Leniency flag that allows libtiff-like TIFF color interpretation behavior
lenient: bool, lenient: bool,
compression: stream::CompressionEngines,
} }
/// All the information needed to read and interpret byte slices from the underlying file, i.e. to /// All the information needed to read and interpret byte slices from the underlying file, i.e. to
@@ -294,8 +295,8 @@ fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
acc.copy_from_slice(&buf[..N]); acc.copy_from_slice(&buf[..N]);
// Process N bytes per iteration. Each channel's prefix sum is independent. // Process N bytes per iteration. Each channel's prefix sum is independent.
let chunks = buf[N..].as_chunks_mut::<N>(); let mut chunks = buf[N..].chunks_exact_mut(N);
for chunk in &mut chunks.0.iter_mut() { for chunk in &mut chunks {
for c in 0..N { for c in 0..N {
acc[c] = chunk[c].wrapping_add(acc[c]); acc[c] = chunk[c].wrapping_add(acc[c]);
chunk[c] = acc[c]; chunk[c] = acc[c];
@@ -303,7 +304,7 @@ fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
} }
// Handle trailing bytes (when buf.len() - N is not a multiple of N). // Handle trailing bytes (when buf.len() - N is not a multiple of N).
for (c, byte) in chunks.1.iter_mut().enumerate() { for (c, byte) in chunks.into_remainder().iter_mut().enumerate() {
*byte = byte.wrapping_add(acc[c]); *byte = byte.wrapping_add(acc[c]);
} }
} }
@@ -853,6 +854,7 @@ impl TiffHeader {
image: None, image: None,
scratch: Vec::new(), scratch: Vec::new(),
lenient: false, lenient: false,
compression: stream::CompressionEngines::default(),
} }
} }
} }
@@ -949,12 +951,18 @@ impl<R: Read + Seek> Decoder<R> {
#[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return. #[expect(clippy::unnecessary_unwrap)] // Lifetimes would overlap due to return.
fn ensure_image_and_reader( fn ensure_image_and_reader(
&mut self, &mut self,
) -> TiffResult<(&mut Image, &mut ValueReader<R>, &mut Vec<u8>)> { ) -> TiffResult<(
&mut Image,
&mut ValueReader<R>,
&mut Vec<u8>,
&mut stream::CompressionEngines,
)> {
if self.image.is_some() { if self.image.is_some() {
return Ok(( return Ok((
self.image.as_mut().unwrap(), self.image.as_mut().unwrap(),
&mut self.value_reader, &mut self.value_reader,
&mut self.scratch, &mut self.scratch,
&mut self.compression,
)); ));
} }
@@ -963,6 +971,7 @@ impl<R: Read + Seek> Decoder<R> {
self.image.insert(image), self.image.insert(image),
&mut self.value_reader, &mut self.value_reader,
&mut self.scratch, &mut self.scratch,
&mut self.compression,
)) ))
} }
@@ -1095,6 +1104,7 @@ impl<R: Read + Seek> Decoder<R> {
image: core::mem::take(&mut self.image), image: core::mem::take(&mut self.image),
scratch: core::mem::take(&mut self.scratch), scratch: core::mem::take(&mut self.scratch),
lenient: self.lenient, lenient: self.lenient,
compression: core::mem::take(&mut self.compression),
} }
} }
@@ -1288,7 +1298,7 @@ impl<R: Read + Seek> Decoder<R> {
/// Utility to create a reader like [`Self::read_entry`], from the current IFD. /// Utility to create a reader like [`Self::read_entry`], from the current IFD.
/// ///
/// Returns an `Ok(None)` if the tag is not found in the current directory. See /// Returns an `Ok(None)` if the tag is not found in the current directory. See
/// [`Self::read_entry`] of which this is a simple wrapper. /// [`Self::read_entry´] of which this is a simple wrapper.
pub fn read_tag(&mut self, tag: Tag) -> TiffResult<Option<EntryBytesReader<'_, R>>> { pub fn read_tag(&mut self, tag: Tag) -> TiffResult<Option<EntryBytesReader<'_, R>>> {
match self.current_ifd().find_entry(tag) { match self.current_ifd().find_entry(tag) {
None => Ok(None), None => Ok(None),
@@ -1578,10 +1588,10 @@ impl<R: Read + Seek> Decoder<R> {
chunk_index: u32, chunk_index: u32,
layout: &image::ReadoutLayout, layout: &image::ReadoutLayout,
) -> TiffResult<()> { ) -> TiffResult<()> {
let (image, value_reader, scratch) = self.ensure_image_and_reader()?; let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
let offset = image.chunk_file_range(chunk_index)?.0; let offset = image.chunk_file_range(chunk_index)?.0;
value_reader.reader.goto_offset(offset)?; value_reader.reader.goto_offset(offset)?;
image.expand_chunk(value_reader, buffer, layout, chunk_index, scratch)?; image.expand_chunk(value_reader, buffer, layout, chunk_index, scratch, engines)?;
Ok(()) Ok(())
} }
@@ -1733,7 +1743,7 @@ impl<R: Read + Seek> Decoder<R> {
slice: TiffCodingUnit, slice: TiffCodingUnit,
buffer: &mut [u8], buffer: &mut [u8],
) -> TiffResult<()> { ) -> TiffResult<()> {
let (image, value_reader, scratch) = self.ensure_image_and_reader()?; let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
let (width, height) = image.chunk_data_dimensions(slice.0)?; let (width, height) = image.chunk_data_dimensions(slice.0)?;
let readout = image.readout_for_size(width, height)?; let readout = image.readout_for_size(width, height)?;
@@ -1766,6 +1776,7 @@ impl<R: Read + Seek> Decoder<R> {
readout, readout,
chunk, chunk,
scratch, scratch,
engines,
)?; )?;
} }
@@ -1937,7 +1948,7 @@ impl<R: Read + Seek> Decoder<R> {
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar /// 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. /// images returns an error if the buffer does not fit the required size.
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> { pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
let (image, value_reader, scratch) = self.ensure_image_and_reader()?; let (image, value_reader, scratch, engines) = self.ensure_image_and_reader()?;
let readout = image.readout_for_image()?; let readout = image.readout_for_image()?;
let ref layout @ image::PlaneLayout { let ref layout @ image::PlaneLayout {
@@ -1971,6 +1982,7 @@ impl<R: Read + Seek> Decoder<R> {
readout, readout,
chunk, chunk,
scratch, scratch,
engines,
)?; )?;
} }
} }
+190 -8
View File
@@ -122,9 +122,86 @@ impl<R: Read> EndianReader<R> {
// # READERS // # READERS
// //
#[derive(Default)]
pub(crate) struct CompressionEngines {
#[cfg(feature = "lzw")]
lzw: Option<weezl::decode::Decoder>,
}
impl core::fmt::Debug for CompressionEngines {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut dbg = f.debug_struct("CompressionEngines");
#[cfg(feature = "lzw")]
dbg.field("lzw", &self.lzw.is_some().then_some("<weezl>"));
dbg.finish()
}
}
/// A variant of `io::Read` that takes one block to the end.
///
/// This is derived from `Read` so that any special optimization done by `std` (or any nightly crate
/// for that matter) surely remains usable.
///
/// We want our decompression engines to be much more specialized than the standard library trait.
/// Firstly there is more information available to communicate (the tiff Type, known decompressed
/// size, …) and secondly they can potentially *reset*. This could be solved with an extension trait
/// but since we want to avoid `read` in favor of `read_exact` (or an equivalent if we want to pass
/// in a strided block buffer) a separate trait that's auto derived is favored.
///
/// FIXME: Also consider that we will want to read subsets of samples. Via IO this is done by
/// discarding data *after* it is put into the output buffer. Yet, for some decompression that write
/// alone is considerable work where it could simply skip bits more efficiently. So the reader would
/// likely want to know about a sample mask.
pub trait ReadSamples: Read {
/// Read a row of samples and throw away some additional padding samples at the end.
///
/// This is automatically implemented for `Self: Read` as a copy into `io::Sink`.
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()>;
/// Put this back into our cache of compression engines.
fn cache_or_destroy(self: Box<Self>, _: &mut CompressionEngines) {}
}
/// Note: the `Seek` bound ensures this does not overlap actual decompression.. This is an odd
/// design problem born out of wanting both the underlying `Read` for uncompressed streams due to
/// performance as well as the override to `cache_or_destroy` for all compressed streams.
impl<R: Read + Seek> ReadSamples for R {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
if discard > 0 {
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
}
Ok(())
}
}
/// Type alias for the deflate Reader /// Type alias for the deflate Reader
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>; pub struct DeflateReader<R> {
inner: flate2::read::ZlibDecoder<R>,
}
impl<R: Read> DeflateReader<R> {
pub fn new(inner: R) -> Self {
DeflateReader {
inner: flate2::read::ZlibDecoder::new(inner),
}
}
}
impl<R: Read> Read for DeflateReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<R: Read> ReadSamples for DeflateReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
// //
// ## LZW Reader // ## LZW Reader
@@ -135,12 +212,17 @@ pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
pub struct LZWReader<R: Read> { pub struct LZWReader<R: Read> {
reader: BufReader<Take<R>>, reader: BufReader<Take<R>>,
decoder: weezl::decode::Decoder, decoder: weezl::decode::Decoder,
is_old_style: bool,
} }
#[cfg(feature = "lzw")] #[cfg(feature = "lzw")]
impl<R: Read> LZWReader<R> { impl<R: Read> LZWReader<R> {
/// Wraps a reader /// Wraps a reader
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> { pub fn new(
reader: R,
compressed_length: usize,
engine: &mut CompressionEngines,
) -> LZWReader<R> {
let mut buffered = BufReader::with_capacity( let mut buffered = BufReader::with_capacity(
(32 * 1024).min(compressed_length), (32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()), reader.take(u64::try_from(compressed_length).unwrap()),
@@ -158,26 +240,75 @@ impl<R: Read> LZWReader<R> {
Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0 Ok(head) if head.len() >= 2 && head[0] == 0x00 && head[1] & 0x01 != 0
); );
let configuration = if old_style { let decoder = if old_style {
weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8) weezl::decode::Configuration::new(weezl::BitOrder::Lsb, 8)
.with_yield_on_full_buffer(true)
.build()
} else { } else {
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8) // All standard LZW decoders have the same configuration. We can thus reuse any that we
} // have previously constructed, skipping internal buffer allocation. We only need to
.with_yield_on_full_buffer(true); // reset them to a new stream.
if let Some(mut lzw) = engine.lzw.take() {
lzw.reset();
lzw
} else {
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true)
.build()
}
};
Self { Self {
reader: buffered, reader: buffered,
decoder: configuration.build(), decoder: decoder,
is_old_style: old_style,
} }
} }
fn discard_n(&mut self, mut n: u64) -> io::Result<()> {
let mut buf = [0u8; 2048];
while n > 0 {
let limit = n.min(2048) as usize;
let result = self
.decoder
.decode_bytes(self.reader.fill_buf()?, &mut buf[..limit]);
self.reader.consume(result.consumed_in);
n -= result.consumed_out as u64;
match result.status {
Ok(weezl::LzwStatus::Ok) => {
continue;
}
Ok(weezl::LzwStatus::NoProgress) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"no lzw end code found",
));
}
Ok(weezl::LzwStatus::Done) => {
if n != 0 {
// stream is too short.
return Err(std::io::ErrorKind::UnexpectedEof)?;
}
}
Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
}
}
Ok(())
}
} }
#[cfg(feature = "lzw")] #[cfg(feature = "lzw")]
impl<R: Read> Read for LZWReader<R> { impl<R: Read> Read for LZWReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
loop { loop {
let result = self.decoder.decode_bytes(self.reader.fill_buf()?, buf); let result = self.decoder.decode_bytes(self.reader.fill_buf()?, buf);
self.reader.consume(result.consumed_in); self.reader.consume(result.consumed_in);
let _ = buf.split_off_mut(..result.consumed_out);
match result.status { match result.status {
Ok(weezl::LzwStatus::Ok) => { Ok(weezl::LzwStatus::Ok) => {
@@ -202,6 +333,24 @@ impl<R: Read> Read for LZWReader<R> {
} }
} }
impl<R: Read> ReadSamples for LZWReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
if discard > 0 {
self.discard_n(discard)?;
}
Ok(())
}
fn cache_or_destroy(self: Box<Self>, engines: &mut CompressionEngines) {
if !self.is_old_style && engines.lzw.is_none() {
engines.lzw = Some(self.decoder);
}
}
}
// //
// ## PackBits Reader // ## PackBits Reader
// //
@@ -274,6 +423,14 @@ impl<R: Read> Read for PackBitsReader<R> {
} }
} }
impl<R: Read> ReadSamples for PackBitsReader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB). /// Iterator adapter that reverses bits in each byte for FillOrder=2 (LSB-to-MSB).
#[cfg(feature = "fax")] #[cfg(feature = "fax")]
type FaxBytes<R> = type FaxBytes<R> =
@@ -390,6 +547,14 @@ impl<R: Read> Read for Group4Reader<R> {
} }
} }
impl<R: Read> ReadSamples for Group4Reader<R> {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
/// Owning byte reader for Group 3 decoding: the compressed data is consumed /// Owning byte reader for Group 3 decoding: the compressed data is consumed
/// into an iterator so the reader doesn't borrow the buffer. /// into an iterator so the reader doesn't borrow the buffer.
#[cfg(feature = "fax")] #[cfg(feature = "fax")]
@@ -528,6 +693,14 @@ impl Read for Group3Reader {
} }
} }
impl ReadSamples for Group3Reader {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
#[cfg(feature = "webp")] #[cfg(feature = "webp")]
pub struct WebPReader { pub struct WebPReader {
inner: Cursor<Vec<u8>>, inner: Cursor<Vec<u8>>,
@@ -583,6 +756,15 @@ impl Read for WebPReader {
} }
} }
#[cfg(feature = "webp")]
impl ReadSamples for WebPReader {
fn read_padded_samples(&mut self, buf: &mut [u8], discard: u64) -> io::Result<()> {
self.read_exact(buf)?;
std::io::copy(&mut self.by_ref().take(discard), &mut std::io::sink())?;
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+1 -1
View File
@@ -171,7 +171,7 @@ mod tests {
dir.get(Tag::Unknown(1)) dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain") .expect("tag 1 should be present after this chain")
.count(), .count(),
u64::from(u16::MAX) u16::MAX.into()
); );
} }
+7 -3
View File
@@ -617,22 +617,26 @@ impl ByteOrder {
return; return;
} }
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
match cls { match cls {
EndianBytes::One => { EndianBytes::One => {
// No change needed // No change needed
} }
EndianBytes::Two => { EndianBytes::Two => {
for chunk in buffer.as_chunks_mut::<2>().0 { for chunk in buffer.chunks_exact_mut(2) {
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
*chunk = u16::from_be_bytes(*chunk).to_le_bytes(); *chunk = u16::from_be_bytes(*chunk).to_le_bytes();
} }
} }
EndianBytes::Four => { EndianBytes::Four => {
for chunk in buffer.as_chunks_mut::<4>().0 { for chunk in buffer.chunks_exact_mut(4) {
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
*chunk = u32::from_be_bytes(*chunk).to_le_bytes(); *chunk = u32::from_be_bytes(*chunk).to_le_bytes();
} }
} }
EndianBytes::Eight => { EndianBytes::Eight => {
for chunk in buffer.as_chunks_mut::<8>().0 { for chunk in buffer.chunks_exact_mut(8) {
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
*chunk = u64::from_be_bytes(*chunk).to_le_bytes(); *chunk = u64::from_be_bytes(*chunk).to_le_bytes();
} }
} }
+6 -47
View File
@@ -506,47 +506,6 @@ fn test_tiled_extra_samples_border_u8() {
assert_eq!(data, expected); assert_eq!(data, expected);
} }
#[test]
fn test_tiled_planar_border_u16() {
// A planar (separate) tiled image whose dimensions are not multiples of the tile size.
// The tile grid repeats for every plane, but the chunk (tile) index counts all planes, so
// the border padding of the second plane used to be looked up with the wrong tile index.
// The bottom-right tile of plane 1 then claimed a full tile height, and the decoder ran
// past the end of the output buffer.
//
// Note that `read_image` only decodes the first plane, so this has to go through
// `read_image_to_buffer` to reach the tiles that were mis-sized.
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-planar-2c-16b.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::open(img_file).expect("Cannot create decoder");
decoder.next_image().expect("Cannot read image IFD");
assert_eq!(decoder.dimensions().unwrap(), (30, 20));
assert_eq!(
decoder.colortype().unwrap(),
ColorType::Multiband {
bit_depth: 16,
num_samples: 2
}
);
let mut buffer = DecodingSampleBuffer::U16(Vec::new());
decoder.read_image_to_buffer(&mut buffer).unwrap();
let DecodingSampleBuffer::U16(data) = buffer else {
panic!("Wrong bit depth")
};
assert_eq!(data.len(), 2 * 20 * 30);
let mut expected = Vec::with_capacity(2 * 20 * 30);
for plane in 0..2u16 {
for y in 0..20u16 {
for x in 0..30u16 {
expected.push(1000 * plane + 10 * y + x);
}
}
}
assert_eq!(data, expected);
}
#[test] #[test]
fn test_inner_access() { fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif"); let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
@@ -939,22 +898,22 @@ fn test_image_bytes(
} }
fn byte_order_u16(bytes: &mut [u8]) { fn byte_order_u16(bytes: &mut [u8]) {
for chunk in bytes.as_chunks_mut::<2>().0 { for chunk in bytes.chunks_exact_mut(2) {
let n = u16::from_ne_bytes(*chunk); let n = u16::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes()); chunk.copy_from_slice(&n.to_le_bytes());
} }
} }
fn byte_order_u32(bytes: &mut [u8]) { fn byte_order_u32(bytes: &mut [u8]) {
for chunk in bytes.as_chunks_mut::<4>().0 { for chunk in bytes.chunks_exact_mut(4) {
let n = u32::from_ne_bytes(*chunk); let n = u32::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes()); chunk.copy_from_slice(&n.to_le_bytes());
} }
} }
fn byte_order_u64(bytes: &mut [u8]) { fn byte_order_u64(bytes: &mut [u8]) {
for chunk in bytes.as_chunks_mut::<8>().0 { for chunk in bytes.chunks_exact_mut(8) {
let n = u64::from_ne_bytes(*chunk); let n = u64::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes()); chunk.copy_from_slice(&n.to_le_bytes());
} }
} }
Binary file not shown.