Compare commits

...
Author SHA1 Message Date
A. Molzer ab0ce47230 Track non-image Directory sequence in Reader
A bit nasty, since we are storing the current Directory within the Image
instead of a separate attribute so we can not have one without the other
directly. Instead we prioritize with another attribute.
2025-07-25 21:26:57 +02:00
A. Molzer f993cfb940 Check full subsubifds structure 2025-07-25 21:21:37 +02:00
A. Molzer 8c4bd58493 Complement directory reading methods by iteration 2025-07-25 21:21:37 +02:00
A. Molzer c073841a9d Add Decoder::restart_ifd
This serves two purposes: reliably resetting the state of the decoder
after kinds of failure; and moving the decoder to a SubIFD chain.
2025-07-25 21:21:37 +02:00
Aurelia Molzer 36a16db659 Merge pull request #285 from image-rs/read-into-buffer
Read into byte buffer
2025-07-25 19:16:09 +00:00
A. Molzer d70c31dcbf Add a Bug note about planar configuration 2025-07-25 21:13:41 +02:00
Aurelia Molzer b589d10a27 Merge pull request #289 from aotimme/decoder-inner
Add inner() method to decoder to provide access to raw reader
2025-07-22 20:18:56 +02:00
Alden Timme f4378eeb80 Also split test_planar_rgb_u8 test 2025-07-22 08:48:57 -07:00
Alden Timme 49015c6e93 Move into separate test 2025-07-22 08:42:39 -07:00
Alden Timme 29402875cb Add inner() method to decoder to provide access to raw reader 2025-07-21 20:47:47 -07:00
Aurelia Molzer 33453289b6 Merge pull request #287 from image-rs/icc-tag
Add ICC (color profile) to the set of known tags
2025-07-19 18:17:56 +02:00
A. Molzer ce0ca5c234 Add ICC (color profile) to the set of known tags 2025-07-19 18:12:54 +02:00
A. Molzer f671293bd8 Fix byte-crc test for big endian platforms 2025-07-19 18:08:02 +02:00
A. Molzer 48d52bed9b Return buffer layout as a struct
Prepares extensibility for the eventual case of supporting multi-planar
image layouts that are to be written into a multi-planar buffer as
indicated by the caller in some way (possibly not via read_*_bytes but a
method taking a wrapper around the output buffer instead). Through the
struct we can later add an indicator that can be used by the caller to
decide between methods of fetching the pixel data as appropriate.
2025-07-19 17:46:37 +02:00
Aurelia Molzer 7707060982 Merge pull request #286 from image-rs/clippy
Resolve all the clippy lints and add it to CI
2025-07-19 17:39:26 +02:00
A. Molzer c370ac4b92 Take row stride input in chunk bytes 2025-07-19 00:12:43 +02:00
A. Molzer b2bcbbaf23 Enable clippy for the merge workflow 2025-07-14 19:56:06 +02:00
A. Molzer d07da5a6cb Resolve clippy lints in tests 2025-07-14 19:56:06 +02:00
A. Molzer a3c81e9830 Add Directory::is_empty for completeness 2025-07-14 19:56:06 +02:00
A. Molzer 46e51cc67e Make documentation adhere clippy, more natural 2025-07-14 19:56:06 +02:00
A. Molzer f6a762b72c Upgrade usage of format! and Default 2025-07-14 19:56:06 +02:00
A. Molzer 23672b2490 Switch to div_ceil for rounding implementation 2025-07-14 19:25:12 +02:00
A. Molzer ca74c03520 Fix straightforward cast/borrow lints 2025-07-14 19:25:12 +02:00
A. Molzer 2942f9de97 Mark f16 bytecast as expected unused 2025-07-14 19:25:12 +02:00
A. Molzer 29add10ca7 Fix manual non_exhaustive pattern 2025-07-14 18:24:01 +02:00
A. Molzer 59d32a66a7 Add test coverage for to byte decoding 2025-07-14 18:23:30 +02:00
A. Molzer 5a6b2ad8f3 Complementary methods reading into borrowed buffer 2025-07-14 18:23:30 +02:00
A. Molzer c746653269 Add Decoder::read_image_bytes, borrowed output 2025-07-14 18:23:30 +02:00
Aurelia Molzer cc75d051af Merge pull request #283 from image-rs/write-directory
Write extra directories
2025-07-14 00:01:39 +02:00
Aurelia Molzer 2e51ee4b15 Merge pull request #282 from image-rs/write-directory
Refactor encoding of directories
2025-07-13 22:28:05 +02:00
18 changed files with 804 additions and 164 deletions

No files matched your search

+4
View File
@@ -45,6 +45,10 @@ jobs:
- name: Cargo check
run: cargo check --features "${{ matrix.feature }}" --no-default-features
- name: Cargo clippy
if: ${{ matrix.feature != '' }}
run: cargo clippy --no-deps --all-features --all-targets -- -D warnings
other_archs:
# github actions does not support 32-bit or big endian systems directly, but
# it does support QEMU. so we install qemu, then build and run the tests on
+2
View File
@@ -27,6 +27,8 @@ zune-jpeg = { version = "0.4.17", optional = true }
[dev-dependencies]
criterion = "0.3.1"
# We may already depend on this via flate2
crc32fast = "1"
[features]
default = ["deflate", "jpeg", "lzw"]
+3
View File
@@ -10,6 +10,9 @@
//! the unsafe code guidelines).
//!
//! TODO: Would like to use std-lib here.
// Until we implement predictors for f16, we don't need f16_as_ne_bytes. (Due to the macro we do
// not apply this directly to the functions). And rust-version is not new enough for `expect`.
#![allow(dead_code)]
use std::{mem, slice};
use half::f16;
+1 -1
View File
@@ -445,7 +445,7 @@ impl Entry {
// 2b: the value is at most 4 bytes or doesn't fit in the offset field.
return Ok(match self.type_ {
Type::BYTE => Byte(self.offset[0]),
Type::SBYTE => SignedByte(i8::from(self.offset[0] as i8)),
Type::SBYTE => SignedByte(self.offset[0] as i8),
Type::UNDEFINED => Byte(self.offset[0]),
Type::SHORT => Short(self.r(bo).read_u16()?),
Type::SSHORT => SignedShort(self.r(bo).read_i16()?),
+38 -13
View File
@@ -10,6 +10,7 @@ use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{self, Cursor, Read, Seek};
use std::num::NonZeroUsize;
use std::sync::Arc;
#[derive(Debug)]
@@ -29,10 +30,10 @@ pub(crate) struct TileAttributes {
impl TileAttributes {
pub fn tiles_across(&self) -> usize {
(self.image_width + self.tile_width - 1) / self.tile_width
self.image_width.div_ceil(self.tile_width)
}
pub fn tiles_down(&self) -> usize {
(self.image_height + self.tile_length - 1) / self.tile_length
self.image_height.div_ceil(self.tile_length)
}
fn padding_right(&self) -> usize {
(self.tile_width - self.image_width % self.tile_width) % self.tile_width
@@ -85,7 +86,19 @@ impl Image {
decoder: &mut ValueReader<R>,
ifd: Directory,
) -> TiffResult<Image> {
let mut tag_reader = TagReader { decoder, ifd: &ifd };
let img = Self::from_ref(decoder, &ifd)?;
Ok(Image {
ifd: Some(ifd),
..img
})
}
pub(crate) fn from_ref<R: Read + Seek>(
decoder: &mut ValueReader<R>,
ifd: &Directory,
) -> TiffResult<Image> {
let mut tag_reader = TagReader { decoder, ifd };
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
@@ -287,7 +300,7 @@ impl Image {
};
Ok(Image {
ifd: Some(ifd),
ifd: None,
width,
height,
bits_per_sample: bits_per_sample[0],
@@ -363,6 +376,22 @@ impl Image {
}
}
pub(crate) fn minimum_row_stride(&self, dims: (u32, u32)) -> Option<NonZeroUsize> {
let (width, height) = dims;
let row_stride = u64::from(width)
.saturating_mul(self.samples_per_pixel() as u64)
.saturating_mul(self.bits_per_sample as u64)
.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,
@@ -625,12 +654,12 @@ impl Image {
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 + 7) / 8).try_into()?;
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(samples as u64)
.ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = ((data_row_bits + 7) / 8).try_into()?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
// TODO: Should these return errors instead?
assert!(output_row_stride >= data_row_bytes);
@@ -643,11 +672,11 @@ impl Image {
self.jpeg_tables.as_deref().map(|a| &**a),
)?;
if output_row_stride == chunk_row_bytes as usize {
if output_row_stride == chunk_row_bytes {
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?;
for row in tile.chunks_mut(chunk_row_bytes as usize) {
for row in tile.chunks_mut(chunk_row_bytes) {
super::fix_endianness_and_predict(
row,
color_type.bit_depth(),
@@ -678,11 +707,7 @@ impl Image {
}
}
} else {
for (_i, row) in buf
.chunks_mut(output_row_stride)
.take(data_dims.1 as usize)
.enumerate()
{
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
let row = &mut row[..data_row_bytes];
reader.read_exact(row)?;
+411 -96
View File
@@ -1,5 +1,7 @@
use std::alloc::{Layout, LayoutError};
use std::collections::BTreeMap;
use std::io::{self, Read, Seek};
use std::num::NonZeroUsize;
use crate::tags::{
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
@@ -21,7 +23,7 @@ mod stream;
mod tag_reader;
/// Result of a decoding process
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum DecodingResult {
/// A vector of unsigned bytes
U8(Vec<u8>),
@@ -197,6 +199,88 @@ impl<'a> DecodingBuffer<'a> {
}
}
/// Information on the byte buffer that should be supplied to the decoder.
///
/// This is relevant for [`Decoder::read_image_bytes`] and [`Decoder::read_chunk_bytes`] where the
/// caller provided buffer must fit the expectations of the decoder to be filled with data from the
/// current image.
#[non_exhaustive]
pub struct BufferLayoutPreference {
/// Minimum byte size of the buffer to read image data.
pub len: usize,
/// Minimum number of bytes to represent a row of image data of the requested content.
pub row_stride: Option<NonZeroUsize>,
}
/// The count and matching discriminant for a `DecodingBuffer`.
#[derive(Clone)]
enum DecodingExtent {
U8(usize),
U16(usize),
U32(usize),
U64(usize),
F16(usize),
F32(usize),
F64(usize),
I8(usize),
I16(usize),
I32(usize),
I64(usize),
}
impl DecodingExtent {
fn to_result_buffer(&self, limits: &Limits) -> TiffResult<DecodingResult> {
match *self {
DecodingExtent::U8(count) => DecodingResult::new_u8(count, limits),
DecodingExtent::U16(count) => DecodingResult::new_u16(count, limits),
DecodingExtent::U32(count) => DecodingResult::new_u32(count, limits),
DecodingExtent::U64(count) => DecodingResult::new_u64(count, limits),
DecodingExtent::F16(count) => DecodingResult::new_f16(count, limits),
DecodingExtent::F32(count) => DecodingResult::new_f32(count, limits),
DecodingExtent::F64(count) => DecodingResult::new_f64(count, limits),
DecodingExtent::I8(count) => DecodingResult::new_i8(count, limits),
DecodingExtent::I16(count) => DecodingResult::new_i16(count, limits),
DecodingExtent::I32(count) => DecodingResult::new_i32(count, limits),
DecodingExtent::I64(count) => DecodingResult::new_i64(count, limits),
}
}
fn preferred_layout(self) -> TiffResult<Layout> {
fn overflow(_: LayoutError) -> TiffError {
TiffError::LimitsExceeded
}
match self {
DecodingExtent::U8(count) => Layout::array::<u8>(count),
DecodingExtent::U16(count) => Layout::array::<u16>(count),
DecodingExtent::U32(count) => Layout::array::<u32>(count),
DecodingExtent::U64(count) => Layout::array::<u64>(count),
DecodingExtent::F16(count) => Layout::array::<f16>(count),
DecodingExtent::F32(count) => Layout::array::<f32>(count),
DecodingExtent::F64(count) => Layout::array::<f64>(count),
DecodingExtent::I8(count) => Layout::array::<i8>(count),
DecodingExtent::I16(count) => Layout::array::<i16>(count),
DecodingExtent::I32(count) => Layout::array::<i32>(count),
DecodingExtent::I64(count) => Layout::array::<i64>(count),
}
.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(())
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
/// Chunk type of the internal representation
pub enum ChunkType {
@@ -206,6 +290,7 @@ pub enum ChunkType {
/// Decoding limits
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Limits {
/// The maximum size of any `DecodingResult` in bytes, the default is
/// 256MiB. If the entire image is decoded at once, then this will
@@ -218,10 +303,6 @@ pub struct Limits {
/// Maximum size for intermediate buffer which may be used to limit the amount of data read per
/// segment even if the entire image is decoded at once.
pub intermediate_buffer_size: usize,
/// The purpose of this is to prevent all the fields of the struct from
/// being public, as this would make adding new fields a major version
/// bump.
_non_exhaustive: (),
}
impl Limits {
@@ -234,10 +315,9 @@ impl Limits {
/// naturally, the machine running the program does not have infinite memory.
pub fn unlimited() -> Limits {
Limits {
decoding_buffer_size: usize::max_value(),
ifd_value_size: usize::max_value(),
intermediate_buffer_size: usize::max_value(),
_non_exhaustive: (),
decoding_buffer_size: usize::MAX,
ifd_value_size: usize::MAX,
intermediate_buffer_size: usize::MAX,
}
}
}
@@ -248,7 +328,6 @@ impl Default for Limits {
decoding_buffer_size: 256 * 1024 * 1024,
intermediate_buffer_size: 128 * 1024 * 1024,
ifd_value_size: 1024 * 1024,
_non_exhaustive: (),
}
}
}
@@ -270,6 +349,10 @@ where
ifd_offsets: Vec<IfdPointer>,
/// Map from the ifd into the `ifd_offsets` ordered list.
seen_ifds: cycles::IfdCycles,
/// The directory, if we have not yet read it as an image.
/// This is prioritized _over_ the image. Hence it must _not_ be set if we are currently
/// reading a true image IFD that is instead stored in the `image` attribute.
non_image_ifd: Option<Directory>,
image: Image,
}
@@ -545,6 +628,7 @@ impl<R: Read + Seek> Decoder<R> {
chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(),
},
non_image_ifd: None,
};
decoder.next_image()?;
Ok(decoder)
@@ -574,48 +658,85 @@ impl<R: Read + Seek> Decoder<R> {
/// Loads the IFD at the specified index in the list, if one exists
pub fn seek_to_image(&mut self, ifd_index: usize) -> TiffResult<()> {
// Check whether we have seen this IFD before, if so then the index will be less than the length of the list of ifd offsets
if ifd_index >= self.ifd_offsets.len() {
// We possibly need to load in the next IFD
if self.next_ifd.is_none() {
let ifd = self.seek_to_directory(ifd_index)?;
self.non_image_ifd = None;
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
}
fn seek_to_directory(&mut self, ifd_index: usize) -> TiffResult<Directory> {
if ifd_index < self.ifd_offsets.len() {
// If the index is within the list of ifds then we can load the selected image/IFD
let ifd_offset = self.ifd_offsets[ifd_index];
let ifd = self.value_reader.read_directory(ifd_offset)?;
self.next_ifd = ifd.next();
self.current_ifd = Some(ifd_offset);
return Ok(ifd);
}
// Follow the list until we find the one we want, or we reach the end, whichever happens
// first. How many IFDs to read only for their `next` field?
let step_over = self.ifd_offsets.len() - ifd_index;
for _ in 0..step_over {
// FIXME: for optimization we only need to read the offset of this one, not its whole
// data. However on buffered files this should be a rather small difference unless
// you're traversing a lot of directories.
// Detecting an end-of-file is done by `next_ifd`. We ignore the IFD itself (but not
// via an ignore pattern to avoid silencing must_use accidentally).
self.next_ifd()?;
}
// self.next_ifd, self.current_ifd will be setup by `next_ifd`.
self.next_ifd()
}
/// Start the chain of image directories from a new location.
///
/// This enters a new chain of image file directories with the indicated offset as the new root
/// (similar to the file having its initial offset at the given IFD position). After this call,
/// [`Self::seek_to_image`] has the new root at index `0` and works relative to the new root.
///
/// This is not atomic with regards to errors. If the function returns an error, the decoder is
/// left in an intermediate state where it does not point to any image. A valid state can be
/// recovered by calling [`Self::restart_at_image`] with a valid offset and a successful seek.
pub fn restart_at_image(&mut self, offset: IfdPointer) -> TiffResult<()> {
let ifd = self.restart_at_offset(offset)?;
self.non_image_ifd = None;
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
}
/// Start the chain of non-image directories from a new location.
///
/// See [`Self::restart_at_image`] for details, except this method does not attempt to
/// interpret the directory in the sequence as an image. Instead, it may be used to read a
/// sequence of auxiliary IFDs that are not necessarily images. For instance, a directory
/// referred to in the SubIfd tag may be a thumbnail
pub fn restart_at_directory(&mut self, offset: IfdPointer) -> TiffResult<()> {
let ifd = self.restart_at_offset(offset)?;
self.non_image_ifd = Some(ifd);
Ok(())
}
fn restart_at_offset(&mut self, offset: IfdPointer) -> TiffResult<Directory> {
self.ifd_offsets.clear();
self.ifd_offsets.push(offset);
self.next_ifd = Some(offset);
self.current_ifd = None;
return Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
));
}
loop {
// Follow the list until we find the one we want, or we reach the end, whichever happens first
let ifd = self.next_ifd()?;
if ifd.next().is_none() {
break;
}
if ifd_index < self.ifd_offsets.len() {
break;
}
}
}
// If the index is within the list of ifds then we can load the selected image/IFD
if let Some(ifd_offset) = self.ifd_offsets.get(ifd_index) {
let ifd = self.value_reader.read_directory(*ifd_offset)?;
self.next_ifd = ifd.next();
self.current_ifd = Some(*ifd_offset);
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
} else {
Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
))
}
self.next_ifd()
}
fn next_ifd(&mut self) -> TiffResult<Directory> {
let Some(next_ifd) = self.next_ifd.take() else {
self.current_ifd = None;
self.non_image_ifd = None;
return Err(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
));
@@ -642,10 +763,44 @@ impl<R: Read + Seek> Decoder<R> {
/// To determine whether there are more images call `TIFFDecoder::more_images` instead.
pub fn next_image(&mut self) -> TiffResult<()> {
let ifd = self.next_ifd()?;
self.non_image_ifd = None;
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
Ok(())
}
/// Read the next directory without interpreting it as an image.
///
/// If there is no further image in the TIFF file a format error is returned. To determine
/// whether there are more images call `TIFFDecoder::more_directories` instead.
pub fn next_directory(&mut self) -> TiffResult<()> {
let ifd = self.next_ifd()?;
self.non_image_ifd = Some(ifd);
Ok(())
}
/// Interpret the current directory as an image.
///
/// This method is used after having called [`Self::restart_at`] or [`Self::next_directory`] to
/// iterate the sequence of image file directories, having read a directory without having read
/// its tags as image data.
pub fn current_directory_as_image(&mut self) -> TiffResult<()> {
let current_ifd = self.current_ifd.ok_or(TiffError::FormatError(
TiffFormatError::ImageFileDirectoryNotFound,
))?;
if let Some(ifd) = &self.non_image_ifd {
self.image = Image::from_ref(&mut self.value_reader, ifd)?;
// Definitely sets an IFD but this works without unwraps.
self.image.ifd = self.non_image_ifd.take();
} else {
// Probably re-reading an image but that's fine.
let ifd = self.read_directory(current_ifd)?;
self.image = Image::from_reader(&mut self.value_reader, ifd)?;
}
Ok(())
}
/// Returns `true` if there is at least one more image available.
pub fn more_images(&self) -> bool {
self.next_ifd.is_some()
@@ -665,6 +820,11 @@ impl<R: Read + Seek> Decoder<R> {
}
}
/// Returns a mutable reference to the stream being decoded.
pub fn inner(&mut self) -> &mut R {
self.value_reader.reader.inner()
}
/// Reads a TIFF byte value
#[inline]
pub fn read_byte(&mut self) -> Result<u8, io::Error> {
@@ -837,17 +997,26 @@ impl<R: Read + Seek> Decoder<R> {
chunk_index: u32,
output_width: usize,
) -> TiffResult<()> {
let offset = self.image.chunk_file_range(chunk_index)?.0;
self.goto_offset_u64(offset)?;
let output_row_stride = (output_width as u64)
.saturating_mul(self.image.samples_per_pixel() as u64)
.saturating_mul(self.image.bits_per_sample as u64)
/ 8;
.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,
) -> 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.as_bytes_mut(),
buffer,
output_row_stride.try_into()?,
chunk_index,
)?;
@@ -856,56 +1025,109 @@ impl<R: Read + Seek> Decoder<R> {
}
fn result_buffer(&self, width: usize, height: usize) -> TiffResult<DecodingResult> {
let bits_per_sample = self.image().bits_per_sample;
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<DecodingExtent> {
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) + 7) / 8)
((width as u64) * bits_per_sample as u64)
.div_ceil(8)
.try_into()
.map_err(|_| TiffError::LimitsExceeded)?
};
let buffer_size = row_samples
.checked_mul(height)
.and_then(|x| x.checked_mul(self.image().samples_per_pixel()))
.and_then(|x| x.checked_mul(self.image.samples_per_pixel()))
.ok_or(TiffError::LimitsExceeded)?;
let max_sample_bits = self.image().bits_per_sample;
let limits = &self.value_reader.limits;
match self.image().sample_format {
SampleFormat::Uint => match max_sample_bits {
n if n <= 8 => DecodingResult::new_u8(buffer_size, limits),
n if n <= 16 => DecodingResult::new_u16(buffer_size, limits),
n if n <= 32 => DecodingResult::new_u32(buffer_size, limits),
n if n <= 64 => DecodingResult::new_u64(buffer_size, limits),
n => Err(TiffError::UnsupportedError(
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 max_sample_bits {
16 => DecodingResult::new_f16(buffer_size, limits),
32 => DecodingResult::new_f32(buffer_size, limits),
64 => DecodingResult::new_f64(buffer_size, limits),
n => Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
)),
},
SampleFormat::Int => match max_sample_bits {
n if n <= 8 => DecodingResult::new_i8(buffer_size, limits),
n if n <= 16 => DecodingResult::new_i16(buffer_size, limits),
n if n <= 32 => DecodingResult::new_i32(buffer_size, limits),
n if n <= 64 => DecodingResult::new_i64(buffer_size, limits),
n => Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
)),
},
format => Err(TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into()),
))
}
},
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
/// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of
/// 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<BufferLayoutPreference> {
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 row_stride = self.image.minimum_row_stride(data_dims);
Ok(BufferLayoutPreference {
len: layout.size(),
row_stride,
})
}
/// 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`.
pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult<DecodingResult> {
let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
@@ -916,6 +1138,32 @@ impl<R: Read + Seek> Decoder<R> {
Ok(result)
}
/// Read the specified chunk (at index `chunk_index`) into an allocated buffer.
///
/// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a
/// 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.
///
/// # 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)?;
let extent = self.result_extent(data_dims.0 as usize, data_dims.1 as usize)?;
extent.assert_layout(buffer.len())?;
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);
self.read_chunk_to_bytes(buffer, chunk_index, output_row_stride)?;
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) {
@@ -930,13 +1178,69 @@ impl<R: Read + Seek> Decoder<R> {
.expect("invalid chunk_index")
}
/// Returns the preferred buffer required to read the whole image with [`Self::read_image_bytes`].
///
/// Returns the layout without being specific as to the underlying type for forward
/// compatibility. Note that, in general, a TIFF may contain an almost arbitrary number of
/// 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_buffer_layout(&mut self) -> TiffResult<BufferLayoutPreference> {
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,
})
}
/// Decodes the entire image and return it 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`.
pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let width = self.image().width;
let height = self.image().height;
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.
///
/// 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
///
/// 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_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
let width = self.image().width;
let height = self.image().height;
let extent = self.result_extent(width as usize, height as usize)?;
extent.assert_layout(buffer.len())?;
if width == 0 || height == 0 {
return Ok(result);
return Ok(());
}
let chunk_dimensions = self.image().chunk_dimensions()?;
@@ -960,12 +1264,12 @@ impl<R: Read + Seek> Decoder<R> {
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 + 7) / 8).try_into()?;
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 + 7) / 8).try_into()?;
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize;
@@ -989,21 +1293,32 @@ impl<R: Read + Seek> Decoder<R> {
y * output_row_stride * chunk_dimensions.1 as usize + x * chunk_row_bytes;
self.image.expand_chunk(
&mut self.value_reader,
&mut result.as_buffer(0).as_bytes_mut()[buffer_offset..],
&mut buffer[buffer_offset..],
output_row_stride,
chunk as u32,
)?;
}
Ok(result)
Ok(())
}
/// Get the IFD decoder for our current image IFD.
fn image_ifd(&mut self) -> IfdDecoder<'_> {
fn current_directory_ifd(&mut self) -> IfdDecoder<'_> {
// Special fallback. We do not want to error handle not having read a current directory, in
// particular as the behavior having a directory without tags will produce these errors
// anyways (most likely). Note that an empty directory is invalid in a TIFF.
static NO_IFD: Directory = Directory::empty();
let ifd = self
.non_image_ifd
.as_ref()
.or(self.image.ifd.as_ref())
.unwrap_or(&NO_IFD);
IfdDecoder {
inner: tag_reader::TagReader {
decoder: &mut self.value_reader,
ifd: self.image.ifd.as_ref().unwrap(),
ifd,
},
}
}
@@ -1044,25 +1359,25 @@ impl<R: Read + Seek> Decoder<R> {
/// Tries to retrieve a tag from the current image directory.
/// Return `Ok(None)` if the tag is not present.
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
self.image_ifd().find_tag(tag)
self.current_directory_ifd().find_tag(tag)
}
/// Tries to retrieve a tag in the current image directory and convert it to the desired
/// unsigned type.
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
self.image_ifd().find_tag_unsigned(tag)
self.current_directory_ifd().find_tag_unsigned(tag)
}
/// Tries to retrieve a tag from the current image directory and convert it to the desired
/// unsigned type. Returns an error if the tag is not present.
pub fn get_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<T> {
self.image_ifd().get_tag_unsigned(tag)
self.current_directory_ifd().get_tag_unsigned(tag)
}
/// Tries to retrieve a tag from the current image directory.
/// Returns an error if the tag is not present
pub fn get_tag(&mut self, tag: Tag) -> TiffResult<ifd::Value> {
self.image_ifd().get_tag(tag)
self.current_directory_ifd().get_tag(tag)
}
pub fn get_tag_u32(&mut self, tag: Tag) -> TiffResult<u32> {
@@ -1117,7 +1432,7 @@ impl<R: Read + Seek> Decoder<R> {
}
pub fn tag_iter(&mut self) -> impl Iterator<Item = TiffResult<(Tag, ifd::Value)>> + '_ {
self.image_ifd().tag_iter()
self.current_directory_ifd().tag_iter()
}
}
+11 -13
View File
@@ -135,20 +135,17 @@ impl<R: Read> EndianReader<R> {
}
}
///
/// # READERS
///
///
/// ## Deflate Reader
///
//
// # READERS
//
/// Type alias for the deflate Reader
#[cfg(feature = "deflate")]
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
///
/// ## LZW Reader
///
//
// ## LZW Reader
//
/// Reader that decompresses LZW streams
#[cfg(feature = "lzw")]
@@ -207,10 +204,11 @@ impl<R: Read> Read for LZWReader<R> {
}
}
///
/// ## PackBits Reader
///
//
// ## PackBits Reader
//
/// Internal state machine for the PackBitsReader.
enum PackBitsReaderState {
Header,
Literal,
+9 -1
View File
@@ -27,7 +27,9 @@ pub struct Directory {
}
impl Directory {
pub fn empty() -> Self {
/// Create a directory in an initial state without entries. Note that an empty directory can
/// not be encoded in a file, it must contain at least one entry.
pub const fn empty() -> Self {
Directory {
entries: BTreeMap::new(),
next_ifd: None,
@@ -72,6 +74,12 @@ impl Directory {
self.entries.len()
}
/// Check if there are any entries in this directory. Note that an empty directory can not be
/// encoded in the file, it must contain at least one entry.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Get the pointer to the next IFD, if it was defined.
pub fn next(&self) -> Option<IfdPointer> {
self.next_ifd.map(|n| IfdPointer(n.get()))
+2 -6
View File
@@ -11,21 +11,17 @@ pub struct Deflate {
/// It allows trading compression ratio for compression speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
#[derive(Default)]
pub enum DeflateLevel {
/// The fastest possible compression mode.
Fast = 1,
/// The conserative choice between speed and ratio.
#[default]
Balanced = 6,
/// The best compression available with Deflate.
Best = 9,
}
impl Default for DeflateLevel {
fn default() -> Self {
DeflateLevel::Balanced
}
}
impl Deflate {
/// Create a new deflate compressor with a specific level of compression.
pub fn with_level(level: DeflateLevel) -> Self {
+6 -6
View File
@@ -296,7 +296,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
}
let entry = Self::write_value(
&mut self.writer,
self.writer,
&DirectoryEntry {
data_type: <T>::FIELD_TYPE,
count: value.count().try_into()?,
@@ -370,14 +370,14 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
if bytes.len() > in_entry_bytes {
let offset = writer.offset();
writer.write_bytes(&bytes)?;
writer.write_bytes(bytes)?;
let offset = K::convert_offset(offset)?;
offset_bytes[..offset.bytes()].copy_from_slice(&offset.data());
} else {
// Note: we have indicated our native byte order in the header, hence this
// corresponds to our byte order no matter the value type.
offset_bytes[..bytes.len()].copy_from_slice(&bytes);
offset_bytes[..bytes.len()].copy_from_slice(bytes);
}
// Undoing some hidden type. Offset is either u32 or u64. Due to the trait API being public
@@ -532,11 +532,11 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
let rows_per_strip = {
match compression.tag() {
CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries
_ => (1_000_000 + row_bytes - 1) / row_bytes,
_ => 1_000_000_u64.div_ceil(row_bytes),
}
};
let strip_count = (u64::from(height) + rows_per_strip - 1) / rows_per_strip;
let strip_count = u64::from(height).div_ceil(rows_per_strip);
encoder.write_tag(Tag::ImageWidth, width)?;
encoder.write_tag(Tag::ImageLength, height)?;
@@ -705,7 +705,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
self.encoder.write_tag(Tag::RowsPerStrip, value)?;
let value: u64 = value as u64;
self.strip_count = (self.height as u64 + value - 1) / value;
self.strip_count = (self.height as u64).div_ceil(value);
self.rows_per_strip = value;
Ok(())
+1 -1
View File
@@ -450,7 +450,7 @@ impl TiffValue for str {
}
}
impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
const BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE;
+3
View File
@@ -184,6 +184,9 @@ quick_error! {
PredictorUnavailable {
display("the requested predictor is not available")
}
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
}
}
+20 -10
View File
@@ -2,7 +2,7 @@ macro_rules! tags {
{
// Permit arbitrary meta items, which include documentation.
$( #[$enum_attr:meta] )*
$vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* {
$vis:vis enum $name:ident($ty:tt) $(unknown(#[$unknown_meta:meta] $unknown_doc:ident))* {
// Each of the `Name = Val,` permitting documentation.
$($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
}
@@ -13,7 +13,7 @@ macro_rules! tags {
pub enum $name {
$($(#[$ident_attr])* $tag,)*
$(
#[doc = $unknown_doc]
#[$unknown_meta]
Unknown($ty),
)*
}
@@ -31,7 +31,7 @@ macro_rules! tags {
fn __to_inner_type(&self) -> $ty {
match *self {
$( $name::$tag => $val, )*
$( $name::Unknown(n) => { $unknown_doc; n }, )*
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
}
}
}
@@ -39,7 +39,7 @@ macro_rules! tags {
tags!($name, $ty, $($unknown_doc)*);
};
// For u16 tags, provide direct inherent primitive conversion methods.
($name:tt, u16, $($unknown_doc:literal)*) => {
($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name {
#[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> {
@@ -48,9 +48,8 @@ macro_rules! tags {
$(
#[inline(always)]
pub fn from_u16_exhaustive(val: u16) -> Self {
$unknown_doc;
Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val))
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
}
)*
@@ -68,7 +67,10 @@ macro_rules! tags {
// Note: These tags appear in the order they are mentioned in the TIFF reference
tags! {
/// TIFF tags
pub enum Tag(u16) unknown("A private or extension tag") {
pub enum Tag(u16) unknown(
/// A private or extension tag
unknown
) {
// Baseline tags:
Artist = 315,
// grayscale images PhotometricInterpretation 1 or 3
@@ -131,6 +133,8 @@ pub enum Tag(u16) unknown("A private or extension tag") {
ExifDirectory = 0x8769,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
GpsDirectory = 0x8825,
// <https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf>
IccProfile = 34675,
GeoKeyDirectoryTag = 34735, // (SPOT)
GeoDoubleParamsTag = 34736, // (SPOT)
GeoAsciiParamsTag = 34737, // (SPOT)
@@ -192,7 +196,10 @@ pub enum Type(u16) {
tags! {
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
/// for reference.
pub enum CompressionMethod(u16) unknown("A custom compression method") {
pub enum CompressionMethod(u16) unknown(
/// A custom compression method
unknown
) {
None = 1,
Huffman = 2,
Fax3 = 3,
@@ -254,7 +261,10 @@ pub enum ResolutionUnit(u16) {
}
tags! {
pub enum SampleFormat(u16) unknown("An unknown extension sample format") {
pub enum SampleFormat(u16) unknown(
/// An unknown extension sample format
unknown
) {
Uint = 1,
Int = 2,
IEEEFP = 3,
+160 -10
View File
@@ -4,6 +4,7 @@ use tiff::decoder::{ifd, Decoder, DecodingResult};
use tiff::ColorType;
use std::fs::File;
use std::io::{Read, Seek};
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
@@ -45,12 +46,12 @@ fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), expected_type);
assert!(match decoder.read_image() {
assert!(matches!(
decoder.read_image(),
Err(tiff::TiffError::UnsupportedError(
tiff::TiffUnsupportedError::UnsupportedColorType(_),
)) => true,
_ => false,
});
))
));
}
#[test]
@@ -172,7 +173,7 @@ fn test_string_tags() {
&s,
"GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/"
),
_ => assert!(false),
other => unreachable!("Expected Ascii tag from file, got {other:?}"),
};
}
}
@@ -231,6 +232,54 @@ fn test_tiled_rect_rgb_u8() {
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
}
#[test]
fn test_inner_access() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
let c = decoder
.get_tag(tiff::tags::Tag::Compression)
.unwrap()
.into_u16()
.unwrap();
assert_eq!(c, tiff::tags::CompressionMethod::None.to_u16());
// Because the image is uncompressed, reading the first tile directly with the inner reader
// should yield the same result as reading it with the decoder's read_chunk method.
let first_offset = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
.unwrap()
.into_iter()
.next()
.unwrap();
let first_byte_count = decoder
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
.unwrap()
.into_iter()
.next()
.unwrap();
decoder
.inner()
.seek(std::io::SeekFrom::Start(first_offset as u64))
.expect("Cannot seek to tile offset");
let mut buf = vec![0; first_byte_count as usize];
decoder
.inner()
.read_exact(&mut buf)
.expect("Cannot read tile data");
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
assert_eq!(sum, raw_sum);
}
_ => panic!("Wrong bit depth"),
}
}
/* #[test]
fn test_tiled_jpeg_rgb_u8() {
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
@@ -292,6 +341,11 @@ fn test_tiled_incremental() {
#[test]
fn test_planar_rgb_u8() {
test_image_sum_u8("planar-rgb-u8.tif", ColorType::RGB(8), 15417630);
}
#[test]
fn test_read_planar_bands() {
// gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE
let file = "planar-rgb-u8.tif";
let expected_type = ColorType::RGB(8);
@@ -328,8 +382,6 @@ fn test_planar_rgb_u8() {
}
_ => panic!("Wrong bit depth"),
}
test_image_sum_u8(file, ColorType::RGB(8), 15417630);
}
#[test]
@@ -348,7 +400,7 @@ fn test_div_zero() {
match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
unexpected => panic!("Unexpected error {}", unexpected),
unexpected => panic!("Unexpected error {unexpected}"),
}
}
@@ -366,7 +418,7 @@ fn test_too_many_value_bytes() {
match error {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
unexpected => panic!("Unexpected error {unexpected}"),
}
}
@@ -497,7 +549,7 @@ fn timeout() {
match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
e => panic!("Unexpected error {:?}", e),
e => panic!("Unexpected error {e:?}"),
}
}
@@ -522,3 +574,101 @@ fn test_zstd_compression() {
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
}
fn test_image_bytes(
file: &str,
expected: &[(ColorType, u32)],
// Represent all samples as little endian, ensuring check sums on all platforms.
normalize_byte_order: fn(&mut [u8]),
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
let mut buffer = vec![0u8; 0];
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
if idx > 0 {
decoder
.next_image()
.expect("mismatched number of directories");
}
assert_eq!(decoder.colortype().unwrap(), expected_type);
let layout = decoder
.image_buffer_layout()
.expect("Cannot describe buffer layout");
buffer.resize(layout.len, 0u8);
decoder.read_image_bytes(&mut buffer).unwrap();
normalize_byte_order(&mut buffer);
// Well almost a crc..
assert_eq!(crc32fast::hash(&buffer), expected_crc);
}
}
fn byte_order_u16(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(2) {
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u32(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(4) {
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
fn byte_order_u64(bytes: &mut [u8]) {
for chunk in bytes.chunks_exact_mut(8) {
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
chunk.copy_from_slice(&n.to_le_bytes());
}
}
#[test]
fn bytes_cmyk_3c_8b() {
test_image_bytes(
"cmyk-3c-8b.tiff",
&[(ColorType::CMYK(8), 2935032230)],
|_| (),
);
}
#[test]
fn bytes_gray_16b() {
test_image_bytes(
"minisblack-1c-16b.tiff",
&[(ColorType::Gray(16), 3524597615)],
byte_order_u16,
);
}
#[test]
fn bytes_gray_32b() {
test_image_bytes(
"gradient-1c-32b.tiff",
&[(ColorType::Gray(32), 2257036530)],
byte_order_u32,
);
}
#[test]
fn bytes_gray_u64() {
test_image_bytes(
"gradient-1c-64b.tiff",
&[(ColorType::Gray(64), 2156547113)],
byte_order_u64,
);
}
#[test]
fn bytes_gray_f32() {
test_image_bytes(
"gradient-1c-32b-float.tiff",
&[(ColorType::Gray(32), 3232356730)],
byte_order_u32,
);
}
+1 -3
View File
@@ -621,7 +621,7 @@ fn test_rows_per_strip() {
let img2 = [i; 2 * 100];
match decoder.read_chunk(i as u32).unwrap() {
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]),
other => panic!("Incorrect strip type {:?}", other),
other => panic!("Incorrect strip type {other:?}"),
}
}
}
@@ -652,8 +652,6 @@ fn test_auxiliary_directory() {
encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap();
}
drop(tiff);
data.set_position(0);
let mut decoder = Decoder::new(&mut data).unwrap();
+2 -2
View File
@@ -116,7 +116,7 @@ fn encode_decode_with_compression(compression: Compression) {
assert_eq!(
match decoder.read_image() {
Ok(DecodingResult::U16(image_data)) => image_data,
unexpected => panic!("Descoding RGB failed: {:?}", unexpected),
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
},
image_rgb.reference_data()
);
@@ -126,7 +126,7 @@ fn encode_decode_with_compression(compression: Compression) {
assert_eq!(
match decoder.read_image() {
Ok(DecodingResult::U8(image_data)) => image_data,
unexpected => panic!("Decoding grayscale failed: {:?}", unexpected),
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
},
image_grayscale.reference_data()
);
+1 -1
View File
@@ -1,7 +1,7 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder};
use tiff::encoder::{colortype, Predictor, TiffEncoder};
use tiff::ColorType;
use std::fs::File;
+129 -1
View File
@@ -1,4 +1,7 @@
use tiff::decoder::Decoder;
use tiff::{
decoder::Decoder,
tags::{IfdPointer, Tag},
};
// This file has the following IFD structure:
//
@@ -39,3 +42,128 @@ fn decode_seek_chain() {
assert_eq!(Some(offset2), decoder.ifd_pointer());
assert!(!decoder.more_images());
}
#[test]
fn decode_seek_recover() {
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
// Remember the offsets in the first iteration.
let offset0 = decoder.ifd_pointer().expect("First IFD pointer not found");
decoder.next_image().unwrap();
let offset1 = decoder.ifd_pointer().expect("Second IFD pointer not found");
decoder.next_image().unwrap();
let offset2 = decoder.ifd_pointer().expect("Third IFD pointer not found");
// oops!
let fails = decoder.restart_at_image(IfdPointer(0xdead_beef));
assert!(fails.is_err());
// However we can recover by restarting our seek.
decoder
.restart_at_image(offset0)
.expect("Failed to restart IFDs from first image");
assert_eq!(Some(offset0), decoder.ifd_pointer());
decoder.next_image().unwrap();
assert_eq!(Some(offset1), decoder.ifd_pointer());
decoder.next_image().unwrap();
assert_eq!(Some(offset2), decoder.ifd_pointer());
}
#[test]
fn decode_seek_directory() {
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
let offset = decoder.ifd_pointer().expect("First IFD pointer not found");
let img0 = decoder.read_image().unwrap();
decoder.next_image().unwrap();
let img1 = decoder.read_image().unwrap();
assert_ne!(img0, img1);
{
// Due to the special test file structure.
let subifd = decoder
.get_tag(Tag::SubIfd)
.unwrap()
.into_ifd_vec()
.unwrap()[0];
// Let's try to manually interpret this as an image.
decoder
.restart_at_directory(subifd)
.expect("Failed to restart IFDs at SubIfd");
decoder
.current_directory_as_image()
.expect("Failed to read SubIfd image");
assert_eq!(decoder.dimensions().unwrap(), (32, 32));
let subimage = decoder.read_image().unwrap();
// Verify we did not accidentally read another image.
assert_ne!(img0, subimage);
assert_ne!(img1, subimage);
}
// And finally, seek back to the first image.
decoder
.restart_at_image(offset)
.expect("First IFD no longer readable");
assert_eq!(Some(offset), decoder.ifd_pointer());
let img0_try2 = decoder.read_image().unwrap();
assert_eq!(img0, img0_try2);
}
#[test]
fn decode_subifds() {
let explore_chain = |decoder: &mut Decoder<_>, subifds: &mut Vec<_>| {
let mut len = 0usize;
loop {
len += 1;
if let Ok(subifd) = decoder.get_tag(Tag::SubIfd) {
let prelen = subifds.len();
subifds.extend(subifd.into_ifd_vec().expect("Failed to decode SubIfd tag"));
eprintln!("New roots: {:?}", &subifds[prelen..]);
} else {
eprintln!("No roots");
}
if !decoder.more_images() {
break;
}
decoder.next_image().unwrap();
}
len
};
let file = File::open(TEST_IMAGE_SUBIFD).expect("Cannot open test image");
let mut decoder = Decoder::new(file).expect("Invalid format to create decoder");
let mut subifds = vec![];
let mut chain_lengths = vec![];
chain_lengths.push(explore_chain(&mut decoder, &mut subifds));
while !subifds.is_empty() {
let root = subifds.remove(0);
let manual_directory = decoder.read_directory(root).unwrap();
eprintln!("Manual directory: {:?}", manual_directory);
decoder.restart_at_directory(root).unwrap();
let prelen = subifds.len();
chain_lengths.push(explore_chain(&mut decoder, &mut subifds));
eprintln!("Chains roots: {:?}", &subifds[prelen..]);
}
// Traversal Order in this graph:
// Image 0 -> Image 1 -> Image 2
// |-> Image 3
// |-> Image 4 -> Image 5
// | \-> Image 7 -> Image 8
// \-> Image 6
assert_eq!(chain_lengths, [3, 1, 2, 1, 2]);
}