Compare commits

...
55 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
A. Molzer ab4fb4e959 Test and polish encoding extra directories 2025-07-13 23:28:24 +02:00
A. Molzer 7b1683687e Split Encoder::write_directory
There are two kinds of directories: those that are image directories
directly in the TIFF file's main sequence, and directories that follow
the structure but represent auxiliary data. The method will link the
directory into the main sequence but those images have additional
requirement on their IFDs which is not clear to the caller. We make that
explicit and also provide a way to write auxiliary directories.
2025-07-13 22:59:53 +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
A. Molzer f6e2aeffe5 Encode values immediately
This currently avoids buffering the collection, but not buffering them
individually where we still format it into a byte vector.

The main drive here is making the DirectoryEncoder less dependent on the
values. That is, it should be concerned with the Entry representation of
values only. A few decoders expect all values to strictly follow the
image directory and the data layout of such files should be controllable
by the user. This could be achieved by encoding dummy entries which are
later corrected once the actual item is known by seeking back and
writing the directory anew.

By no longer assuming values to be available on finishing a directory we
also prepare an interface that encodes an existing directory.
2025-07-13 03:06:40 +02:00
A. Molzer 525bb2b9b1 Simplify encoder writing of dictionary 2025-07-13 02:22:17 +02:00
A. Molzer 54f733eaa3 Allow non-chained directories in writer
This is just the implementation details. In the DirectoryEncoder we did
not differentiate between different directories that should go into the
main image chain and other directories. This is now possible by storing
the prior offset as an explicit Option<NonZeroU64>.

Additionally, the whole encoder did not allow any intermediate data
between its last directory and new directories on the chain. The offset
of the relevant `next` field was not stored explicitly but derived from
the current stream position at the moment of starting the directory.
This would not allow any padding data or data written outside the
returned `DirectoryEncoder` object.

This relaxed both requirements by turning the offsets into attributes.
2025-07-13 02:22:17 +02:00
Aurelia Molzer 9b0c9ca84c Merge pull request #281 from image-rs/write-directory
Adjust Directory for ease of writing
2025-07-13 01:16:39 +02:00
A. Molzer 890488b16e Add Entry::field_type
The directory API offers a direct access to entries. It is useful for
downstream to have the ability of inspecting fields before consuming
them.
2025-07-13 01:11:59 +02:00
A. Molzer 3e530d27ea Directory compatibility with encoding and BigTiff 2025-07-13 01:11:59 +02:00
Aurelia Molzer 02699c9588 Merge pull request #278 from image-rs/read-directory
Expose methods related to reading directory
2025-07-12 17:23:34 +02:00
A. Molzer 26b6afc787 Document Directory related symbols 2025-07-12 15:34:46 +02:00
Aurelia Molzer 5df003368b Merge pull request #280 from image-rs/status-badge
Let status badge reflect main workflows
2025-07-11 15:17:48 +02:00
A. Molzer a582fdcc72 Let status badge reflect main workflows 2025-07-11 15:14:37 +02:00
A. Molzer e62cdfbf42 Merge TagReader and exposed IfdReader internals
Having the value reader itself take care of the bigtiff and limit
semantics is a little cleaner, with the TagReader being responsible
for the interface of grabbing specific types of IFD values.
2025-07-11 15:09:31 +02:00
A. Molzer f021e3e599 Rename ifd value adapter to read_directory_tags 2025-07-11 14:12:36 +02:00
A. Molzer f5ab3b363b Expose interacting with secondary IFDs 2025-07-11 14:12:36 +02:00
A. Molzer f33c7be21c Add the SubIfd tag 2025-07-11 14:12:36 +02:00
A. Molzer 98cd3dc2d2 Refactor directory reading
This allows us to recombine the reader and other file data in a Decoder
with any IFD, whether that of an image or an extension IFD as indicated
by an IFD pointer such as Exif and GPS.
2025-07-11 14:12:34 +02:00
Aurelia Molzer 6275b7a36d Merge pull request #279 from image-rs/seeking-multiple-times
Seeking multiple times
2025-07-11 13:18:41 +02:00
A. Molzer 97e230584f Test ordering directory IFD 2025-07-10 23:57:25 +02:00
A. Molzer 43d7f66c9e Refactor cycle and IFD tracking
The previous strategy could not keep track of all cycles such as
reflexive ones at the very start. Additionally, it would fail to be able
to iterate after seeking since it was overly cautious and verified the
next IFD position before allowing the read of a current IFD.

The new implementation is furthermore proof against having multiple
independent chains of IFD which may occur when iterating SubIFDs for
thumbnails. (Note that we do not verify the tree-structure, only a
freedom from cycles).
2025-07-10 23:52:07 +02:00
A. Molzer a1be18b548 Add subsubifds test file 2025-07-10 23:48:20 +02:00
Aurelia Molzer df4e42eecc Add a dedicated Directory type (#277) 2025-07-07 19:59:37 -07:00
Vladimir Kuznetsov 816fb85304 Use libtiff compatible weezl configuration (#276) 2025-06-29 10:24:28 -07:00
Jonathan Behrens 7648317643 Stop doing color conversion when decoding JPEG compressed images (#274) 2025-06-22 16:19:36 -07:00
Jonathan Behrens 140f2788de Update CI workflow (#275) 2025-06-22 10:37:30 -07:00
25 changed files with 2024 additions and 563 deletions

No files matched your search

+31 -2
View File
@@ -1,9 +1,9 @@
name: Rust CI
on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
@@ -45,6 +45,35 @@ 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
# emulated systems. NOTE: you can also use this approach to test locally.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [powerpc-unknown-linux-gnu, i686-unknown-linux-gnu]
steps:
- uses: actions/checkout@v4
- name: Install or use cached cross-rs/cross
uses: baptiste0928/cargo-install@v2
with:
crate: cross
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
key: ${{ matrix.arch }}
- name: Start Docker (required for cross-rs)
run: sudo systemctl start docker
- name: Cross-Run Tests using QEMU
run: cross test --target ${{ matrix.arch }}
rustfmt:
runs-on: ubuntu-latest
steps:
+3 -1
View File
@@ -21,12 +21,14 @@ half = { version = "2.4.1" }
quick-error = "2.0.1"
flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.0", optional = true }
weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true }
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"]
+1 -1
View File
@@ -1,5 +1,5 @@
# image-tiff
[![Build Status](https://github.com/image-rs/image-tiff/workflows/Rust%20CI/badge.svg)](https://github.com/image-rs/image-tiff/actions)
[![Build Status](https://github.com/image-rs/image-tiff/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/image-rs/image-tiff/actions)
[![Documentation](https://docs.rs/tiff/badge.svg)](https://docs.rs/tiff)
[![Further crate info](https://img.shields.io/crates/v/tiff.svg)](https://crates.io/crates/tiff)
+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;
+169
View File
@@ -0,0 +1,169 @@
use crate::{tags::IfdPointer, TiffError, TiffFormatError};
use std::collections::HashMap;
/// The IFD structure of a TIFF file should be limited to a forest of entries, and a tree when we
/// only consider having a primary IFD and its children. There is up to one primary child of a node
/// that is the `next` entry in an IFD itself. Since we offer iteration over this chain we want to
/// detect when a new edge would introduce a cycle, such that callers can be guaranteed to
/// terminate iteration at some point even in malicious images.
///
/// However, we are not necessarily visiting the IFDs in their topological order.
///
/// Since we only consider one child, we run union find to assign each pointer to a known chain.
/// Then, when we insert a new edge we check if they belong to the same component.
#[derive(Default, Debug)]
pub struct IfdCycles {
/// The root of each component union.
component_union: HashMap<ComponentId, ComponentId>,
links: HashMap<IfdPointer, u64>,
chains: HashMap<IfdPointer, ComponentId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ComponentId(u64);
impl IfdCycles {
pub fn new() -> Self {
IfdCycles::default()
}
pub fn insert_next(
&mut self,
from: IfdPointer,
to: Option<IfdPointer>,
) -> Result<bool, TiffError> {
let to_offset = to.map_or(0, |p| p.0);
// For some reason we got
match self.links.get(&from) {
Some(existing) if *existing == to_offset => return Ok(false),
// We got here twice with two different reads of the same IFD. That is .. unusual and
// we interpret it as a cycle.
Some(_) => return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets)),
None => self.links.insert(from, to_offset),
};
self.ensure_node(from);
if let Some(to) = to {
self.ensure_node(to);
let parent = self.nominal_component(from);
let child = self.nominal_component(to);
if parent == child {
return Err(TiffError::FormatError(TiffFormatError::CycleInOffsets));
}
self.component_union.insert(child, parent);
}
Ok(true)
}
fn ensure_node(&mut self, ifd: IfdPointer) {
self.chains.entry(ifd).or_insert_with(|| {
let id = ComponentId(self.component_union.len() as u64);
self.component_union.insert(id, id);
id
});
}
fn nominal_component(&mut self, node: IfdPointer) -> ComponentId {
let id = self.chains[&node];
let nomimal = {
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break parent;
}
iter = parent;
}
};
if nomimal != id {
// Compress.
let mut iter = id;
loop {
let parent = self.component_union[&iter];
if parent == iter {
break;
}
self.component_union.insert(iter, nomimal);
iter = parent;
}
}
nomimal
}
}
#[test]
fn cycles_are_detected() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x800)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x800), Some(IfdPointer(0x20)))
.expect_err("cycle must be detected");
}
#[test]
fn reflective_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x20)))
.expect_err("self-referential cycle must be detected");
}
#[test]
fn late_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x60)))
.expect_err("non-existing link is valid");
}
#[test]
fn odd_cycle() {
let mut cycles = IfdCycles::new();
cycles
.insert_next(IfdPointer(0x20), Some(IfdPointer(0x40)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x60), Some(IfdPointer(0x80)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x80), Some(IfdPointer(0x20)))
.expect("non-existing link is valid");
cycles
.insert_next(IfdPointer(0x40), Some(IfdPointer(0x80)))
.expect_err("non-existing link is valid");
}
+49 -5
View File
@@ -6,7 +6,7 @@ use std::mem;
use std::str;
use super::stream::{ByteOrder, EndianReader};
use crate::tags::{Tag, Type};
use crate::tags::{IfdPointer, Tag, Type};
use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{
@@ -130,6 +130,25 @@ impl Value {
}
}
/// Turn this value into an `IfdPointer`.
///
/// Notice that this does not take an argument, a 64-bit IFD is always allowed. If the
/// difference is crucial and you do not want to be permissive you're expected to filter this
/// out before.
///
/// For compatibility the smaller sized tags should always be allowed i.e. you might use a
/// non-bigtiff's directory and its tag types and move it straight to a bigtiff. For instance
/// the SubIFD tag is defined as `LONG or IFD`:
///
/// <https://web.archive.org/web/20181105221012/https://www.awaresystems.be/imaging/tiff/tifftags/subifds.html>
pub fn into_ifd_pointer(self) -> TiffResult<IfdPointer> {
match self {
Unsigned(val) | Ifd(val) => Ok(IfdPointer(val.into())),
IfdBig(val) => Ok(IfdPointer(val)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_string(self) -> TiffResult<String> {
match self {
Ascii(val) => Ok(val),
@@ -300,6 +319,17 @@ impl Value {
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_ifd_vec(self) -> TiffResult<Vec<IfdPointer>> {
let vec = match self {
Unsigned(val) | Ifd(val) => return Ok(vec![IfdPointer(val.into())]),
IfdBig(val) => return Ok(vec![IfdPointer(val)]),
List(vec) => vec,
_ => return Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
};
vec.into_iter().map(Self::into_ifd_pointer).collect()
}
}
#[derive(Clone)]
@@ -320,9 +350,9 @@ impl ::std::fmt::Debug for Entry {
impl Entry {
pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
let mut offset = offset.to_vec();
offset.append(&mut vec![0; 4]);
Entry::new_u64(type_, count.into(), offset[..].try_into().unwrap())
let mut entry_off = [0u8; 8];
entry_off[..4].copy_from_slice(&offset);
Entry::new_u64(type_, count.into(), entry_off)
}
pub fn new_u64(type_: Type, count: u64, offset: [u8; 8]) -> Entry {
@@ -333,6 +363,18 @@ impl Entry {
}
}
pub fn field_type(&self) -> Type {
self.type_
}
pub fn count(&self) -> u64 {
self.count
}
pub(crate) fn offset(&self) -> &[u8] {
&self.offset
}
/// Returns a mem_reader for the offset/value field
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
@@ -403,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()?),
@@ -668,4 +710,6 @@ fn offset_to_sbytes(n: usize, entry: &Entry) -> TiffResult<Value> {
}
/// Type representing an Image File Directory
#[doc(hidden)]
#[deprecated = "Use struct `tiff::Directory` instead which contains all fields relevant to an Image File Directory, including the offset to the next directory"]
pub type Directory = HashMap<Tag, Entry>;
+68 -59
View File
@@ -1,13 +1,16 @@
use super::ifd::{Directory, Value};
use super::stream::{ByteOrder, PackBitsReader};
use super::ifd::Value;
use super::stream::PackBitsReader;
use super::tag_reader::TagReader;
use super::{predict_f16, predict_f32, predict_f64, Limits};
use super::{stream::EndianReader, ChunkType};
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
};
use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
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)]
@@ -27,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
@@ -80,17 +83,22 @@ pub(crate) struct Image {
impl Image {
pub fn from_reader<R: Read + Seek>(
reader: &mut EndianReader<R>,
decoder: &mut ValueReader<R>,
ifd: Directory,
limits: &Limits,
bigtiff: bool,
) -> TiffResult<Image> {
let mut tag_reader = TagReader {
reader,
limits,
ifd: &ifd,
bigtiff,
};
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()?;
@@ -115,7 +123,7 @@ impl Image {
};
let jpeg_tables = if compression_method == CompressionMethod::ModernJPEG
&& ifd.contains_key(&Tag::JPEGTables)
&& ifd.contains(Tag::JPEGTables)
{
let vec = tag_reader
.find_tag(Tag::JPEGTables)?
@@ -210,10 +218,10 @@ impl Image {
let strip_decoder;
let tile_attributes;
match (
ifd.contains_key(&Tag::StripByteCounts),
ifd.contains_key(&Tag::StripOffsets),
ifd.contains_key(&Tag::TileByteCounts),
ifd.contains_key(&Tag::TileOffsets),
ifd.contains(Tag::StripByteCounts),
ifd.contains(Tag::StripOffsets),
ifd.contains(Tag::TileByteCounts),
ifd.contains(Tag::TileOffsets),
) {
(true, true, false, false) => {
chunk_type = ChunkType::Strip;
@@ -292,7 +300,7 @@ impl Image {
};
Ok(Image {
ifd: Some(ifd),
ifd: None,
width,
height,
bits_per_sample: bits_per_sample[0],
@@ -368,9 +376,24 @@ 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,
#[allow(unused_variables)] photometric_interpretation: PhotometricInterpretation,
compression_method: CompressionMethod,
compressed_length: u64,
#[allow(unused_variables)] jpeg_tables: Option<&[u8]>,
@@ -427,30 +450,14 @@ impl Image {
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg_data);
let mut options: zune_core::options::DecoderOptions = Default::default();
match photometric_interpretation {
PhotometricInterpretation::RGB => {
options =
options.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::RGB);
}
PhotometricInterpretation::CMYK => {
options = options
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::CMYK);
}
PhotometricInterpretation::YCbCr => {
options = options
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::YCbCr);
}
PhotometricInterpretation::WhiteIsZero
| PhotometricInterpretation::BlackIsZero
| PhotometricInterpretation::TransparencyMask => {}
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::CIELab => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedInterpretation(
photometric_interpretation,
),
));
}
// Disable color conversion by setting the output colorspace to the input
// colorspace.
decoder.decode_headers()?;
if let Some(colorspace) = decoder.get_input_colorspace() {
options = options.jpeg_set_out_colorspace(colorspace);
}
decoder.set_options(options);
let data = decoder.decode()?;
@@ -555,13 +562,20 @@ impl Image {
pub(crate) fn expand_chunk(
&self,
reader: impl Read,
reader: &mut ValueReader<impl Read>,
buf: &mut [u8],
output_row_stride: usize,
byte_order: ByteOrder,
chunk_index: u32,
limits: &Limits,
) -> TiffResult<()> {
let ValueReader {
reader,
bigtiff: _,
limits,
} = reader;
let byte_order = reader.byte_order;
let reader = reader.inner();
// Validate that the color type is supported.
let color_type = self.colortype()?;
match color_type {
@@ -640,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);
@@ -653,17 +667,16 @@ impl Image {
let mut reader = Self::create_reader(
reader,
photometric_interpretation,
compression_method,
*compressed_bytes,
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(),
@@ -694,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)?;
+829 -341
View File
File diff suppressed because it is too large. Load diff
+20 -16
View File
@@ -18,7 +18,7 @@ pub struct EndianReader<R> {
pub(crate) byte_order: ByteOrder,
}
impl<R: Read + Seek> EndianReader<R> {
impl<R: Read> EndianReader<R> {
pub fn new(reader: R, byte_order: ByteOrder) -> Self {
Self { reader, byte_order }
}
@@ -27,7 +27,10 @@ impl<R: Read + Seek> EndianReader<R> {
&mut self.reader
}
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> {
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
where
R: Seek,
{
self.reader.seek(io::SeekFrom::Start(offset))?;
Ok(())
}
@@ -132,20 +135,17 @@ impl<R: Read + Seek> 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")]
@@ -158,12 +158,15 @@ pub struct LZWReader<R: Read> {
impl<R: Read> LZWReader<R> {
/// Wraps a reader
pub fn new(reader: R, compressed_length: usize) -> LZWReader<R> {
let configuration =
weezl::decode::Configuration::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
.with_yield_on_full_buffer(true);
Self {
reader: BufReader::with_capacity(
(32 * 1024).min(compressed_length),
reader.take(u64::try_from(compressed_length).unwrap()),
),
decoder: weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8),
decoder: configuration.build(),
}
}
}
@@ -201,10 +204,11 @@ impl<R: Read> Read for LZWReader<R> {
}
}
///
/// ## PackBits Reader
///
//
// ## PackBits Reader
//
/// Internal state machine for the PackBitsReader.
enum PackBitsReaderState {
Header,
Literal,
+28 -15
View File
@@ -1,25 +1,37 @@
use std::io::{Read, Seek};
use crate::tags::Tag;
use crate::{tags::Tag, Directory};
use crate::{TiffError, TiffFormatError, TiffResult};
use super::ifd::{Directory, Value};
use super::stream::EndianReader;
use super::Limits;
use super::ifd::{Entry, Value};
use super::ValueReader;
pub(crate) struct TagReader<'a, R: Read + Seek> {
pub reader: &'a mut EndianReader<R>,
pub ifd: &'a Directory,
pub limits: &'a Limits,
pub bigtiff: bool,
pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
pub(crate) decoder: &'lt mut R,
pub(crate) ifd: &'lt Directory,
}
impl<'a, R: Read + Seek> TagReader<'a, R> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
Ok(match self.ifd.get(&tag) {
Some(entry) => Some(entry.clone().val(self.limits, self.bigtiff, self.reader)?),
None => None,
})
pub(crate) trait EntryDecoder {
/// Turn an entry into a value by fetching the required bytes in the stream.
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
}
impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
entry.val(&self.limits, self.bigtiff, &mut self.reader)
}
}
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
pub(crate) fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<Value>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_val(&entry).map(Some)
}
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
match self.find_tag(tag)? {
Some(val) => Ok(val),
@@ -28,6 +40,7 @@ impl<'a, R: Read + Seek> TagReader<'a, R> {
)),
}
}
pub fn find_tag_uint_vec<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<Vec<T>>> {
self.find_tag(tag)?
.map(|v| v.into_u64_vec())
+166
View File
@@ -0,0 +1,166 @@
use core::fmt;
use std::{collections::BTreeMap, num::NonZeroU64};
use crate::{
decoder::ifd::Entry,
tags::{IfdPointer, Tag},
};
/// An Image File Directory (IFD).
///
/// A directory is a map of [`Tag`]s to [`Value`](crate::decoder::ifd::Value)s. The values are
/// stored anywhere in the file, with the directory containing the offsets and length of the
/// associated values for each tag present in the directory.
///
/// A directory can be created with with
/// [`Decoder::read_directory`](crate::decoder::Decoder::read_directory) or as an empty directory
/// to be extended with entries. A directory may be used with
/// [`Decoder::read_directory_tags`](crate::decoder::Decoder::read_directory_tags) to read the
/// values associated with tags from an underlying file.
#[doc(alias = "IFD")]
pub struct Directory {
/// There are at most `u16::MAX` entries in any single directory, the count is stored as a
/// 2-byte value. The order in the file is implied to be ascending by tag value (the decoder
/// does not mind unordered entries).
pub(crate) entries: BTreeMap<u16, Entry>,
pub(crate) next_ifd: Option<NonZeroU64>,
}
impl Directory {
/// 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,
}
}
/// Retrieve the value associated with a tag.
pub fn get(&self, tag: Tag) -> Option<&Entry> {
self.entries.get(&tag.to_u16())
}
/// Check if the directory contains a specified tag.
pub fn contains(&self, tag: Tag) -> bool {
self.entries.contains_key(&tag.to_u16())
}
/// Iterate over all known and unknown tags in this directory.
pub fn iter(&self) -> impl Iterator<Item = (Tag, &Entry)> + '_ {
self.entries
.iter()
.map(|(k, v)| (Tag::from_u16_exhaustive(*k), v))
}
/// Insert additional entries into the directory.
///
/// Note that a directory can contain at most `u16::MAX` values. There may be one entry that
/// does not fit into the directory. This entry is silently ignored (please check [`Self::len`]
/// to detect the condition). Providing a tag multiple times or a tag that already exists
/// within this directory overwrites the entry.
pub fn extend(&mut self, iter: impl IntoIterator<Item = (Tag, Entry)>) {
// Code size conscious extension, avoid monomorphic extensions with the assumption of these
// not being performance sensitive in practice. (Maybe we have a polymorphic interface for
// the crate usage in the future.
self.extend_inner(iter.into_iter().by_ref())
}
/// Get the number of entries.
pub fn len(&self) -> usize {
// The keys are `u16`. Since IFDs are required to have at least one entry this would have
// been a trivial thing to do in the specification by storing it minus one but alas. In
// BigTIFF the count is stored as 8-bit anyways.
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()))
}
pub fn set_next(&mut self, next: Option<IfdPointer>) {
self.next_ifd = next.and_then(|n| NonZeroU64::new(n.0));
}
fn extend_inner(&mut self, iter: &mut dyn Iterator<Item = (Tag, Entry)>) {
for (tag, entry) in iter {
// If the tag is already present, it will be overwritten.
let map_entry = self.entries.entry(tag.to_u16());
match map_entry {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(entry);
}
std::collections::btree_map::Entry::Occupied(mut occupied_entry) => {
occupied_entry.insert(entry);
}
}
}
}
}
impl fmt::Debug for Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory")
.field(
"entries",
&self.entries.iter().map(|(k, v)| (Tag::from_u16(*k), v)),
)
.field("next_ifd", &self.next_ifd)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::Directory;
use crate::{decoder::ifd::Entry, tags::Tag};
#[test]
fn directory_multiple_entries() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
dir.extend((0..=u16::MAX).map(|i| {
let tag = Tag::Unknown(1);
let entry = Entry::new_u64(crate::tags::Type::BYTE, i.into(), [0; 8]);
(tag, entry)
}));
assert_eq!(dir.len(), 1, "Only one tag was ever modified");
assert_eq!(
dir.get(Tag::Unknown(1))
.expect("tag 1 should be present after this chain")
.count(),
u16::MAX.into()
);
}
#[test]
fn iteration_order() {
let mut dir = Directory::empty();
assert_eq!(dir.len(), 0);
let fake_entry = Entry::new_u64(crate::tags::Type::BYTE, 0, [0; 8]);
dir.extend((0..32).map(|i| {
let tag = Tag::Unknown(i);
let entry = fake_entry.clone();
(tag, entry)
}));
let iter_order: Vec<u16> = dir.iter().map(|(tag, _e)| tag.to_u16()).collect();
assert_eq!(
iter_order,
(0..32).collect::<Vec<_>>(),
"Tags must be in ascending order according to the specification"
);
}
}
+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 {
+211 -89
View File
@@ -2,17 +2,17 @@ pub use tiff_value::*;
use std::{
cmp,
collections::BTreeMap,
io::{self, Seek, Write},
marker::PhantomData,
mem,
num::TryFromIntError,
num::{NonZeroU64, TryFromIntError},
};
use crate::{
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{CompressionMethod, ResolutionUnit, SampleFormat, Tag},
TiffError, TiffFormatError,
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
Directory, TiffError, TiffFormatError,
};
pub mod colortype;
@@ -107,6 +107,8 @@ pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
kind: PhantomData<K>,
predictor: Predictor,
compression: Compression,
/// The offset of the last main image directory's `next` field.
last_ifd_chain: NonZeroU64,
}
/// Constructor functions to create standard Tiff files.
@@ -135,16 +137,19 @@ impl<W: Write + Seek> TiffEncoder<W, TiffKindBig> {
impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
/// Creates a new Tiff or BigTiff encoder, inferred from the return type.
pub fn new_generic(writer: W) -> TiffResult<Self> {
let mut encoder = TiffEncoder {
writer: TiffWriter::new(writer),
let mut writer = TiffWriter::new(writer);
K::write_header(&mut writer)?;
let last_ifd_chain = NonZeroU64::new(writer.previous_ifd_pointer::<K>())
.expect("Header is at a non-zero offset");
Ok(TiffEncoder {
writer,
kind: PhantomData,
predictor: Predictor::None,
compression: Compression::Uncompressed,
};
K::write_header(&mut encoder.writer)?;
Ok(encoder)
last_ifd_chain,
})
}
/// Set the predictor to use
@@ -165,8 +170,27 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
}
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<W, K>> {
DirectoryEncoder::new(&mut self.writer)
#[deprecated = "`image_directory` replaced the old behavior and clarifies the intent"]
#[doc(hidden)]
pub fn new_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
///
/// The caller is responsible for ensuring that the directory is a valid image in the main TIFF
/// IFD sequence. To encode additional directories that are not linked into the sequence, use
/// [`Self::extra_directory`][TiffEncoder::extra_directory].
pub fn image_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
}
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
///
/// The directory is not linked into the sequence of directories. For instance, encode Exif
/// directories or SubIfd directories with this method.
pub fn extra_directory(&mut self) -> TiffResult<DirectoryEncoder<'_, W, K>> {
Self::unchained_directory(&mut self.writer)
}
/// Create an [`ImageEncoder`] to encode an image one slice at a time.
@@ -175,7 +199,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
width: u32,
height: u32,
) -> TiffResult<ImageEncoder<W, C, K>> {
let encoder = DirectoryEncoder::new(&mut self.writer)?;
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
}
@@ -189,11 +213,23 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
where
[C::Inner]: TiffValue,
{
let encoder = DirectoryEncoder::new(&mut self.writer)?;
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
let image: ImageEncoder<W, C, K> =
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)?;
image.write_data(data)
}
fn chain_directory<'lt>(
writer: &'lt mut TiffWriter<W>,
last_ifd_chain: &'lt mut NonZeroU64,
) -> TiffResult<DirectoryEncoder<'lt, W, K>> {
let last_ifd = *last_ifd_chain;
DirectoryEncoder::new(writer, Some(last_ifd), Some(last_ifd_chain))
}
fn unchained_directory(writer: &mut TiffWriter<W>) -> TiffResult<DirectoryEncoder<'_, W, K>> {
DirectoryEncoder::new(writer, None, None)
}
}
/// Low level interface to encode ifd directories.
@@ -202,88 +238,77 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
/// Encoding can silently fail while this is dropping.
pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> {
writer: &'a mut TiffWriter<W>,
dropped: bool,
/// The position of the previous directory's `next` field, if any.
chained_ifd_pos: Option<NonZeroU64>,
/// An output to write the `next` field offset on completion.
write_chain: Option<&'a mut NonZeroU64>,
kind: PhantomData<K>,
// We use BTreeMap to make sure tags are written in correct order
ifd_pointer_pos: u64,
ifd: BTreeMap<u16, DirectoryEntry<K::OffsetType>>,
directory: Directory,
dropped: bool,
}
/// The offset of an encoded directory in the file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DirectoryOffset<K: TiffKind> {
/// The start of the directory as a Tiff value.
///
/// This is a bit of a wart in the strongly typed design of the encoder. The value _type_ must
/// itself know how to be represented in the Tiff file but that may differ based on endianess
/// as well as the offset size (`u32` or BigTIFF's `u64`). Thankfully we're allowed to
/// represent offsets with `LONG` or `IFD` in the usual case.
pub offset: K::OffsetType,
/// The start of the directory as a pure offset.
pub pointer: IfdPointer,
/// The offset of its sequence link field, in our private representation.
ifd_chain: NonZeroU64,
/// The kind of Tiff file the offset is for.
kind: PhantomData<K>,
}
impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
fn new(writer: &'a mut TiffWriter<W>) -> TiffResult<Self> {
// the previous word is the IFD offset position
let ifd_pointer_pos = writer.offset() - mem::size_of::<K::OffsetType>() as u64;
/// Construct a directory writer appending to data, assuming the writer is currently positioned
/// immediately after a previously written IFD to which to append.
fn new(
writer: &'a mut TiffWriter<W>,
chained_ifd_pos: Option<NonZeroU64>,
chain_into: Option<&'a mut NonZeroU64>,
) -> TiffResult<Self> {
writer.pad_word_boundary()?; // TODO: Do we need to adjust this for BigTiff?
Ok(DirectoryEncoder {
Ok(Self {
writer,
chained_ifd_pos,
write_chain: chain_into,
kind: PhantomData,
directory: Directory::empty(),
dropped: false,
ifd_pointer_pos,
ifd: BTreeMap::new(),
})
}
/// Write a single ifd tag.
pub fn write_tag<T: TiffValue>(&mut self, tag: Tag, value: T) -> TiffResult<()> {
// Encodes the value if necessary. In the current bytes all integers are taken as native
// endian and thus transparent but this keeps the interface generic.
let mut bytes = Vec::with_capacity(value.bytes());
{
let mut writer = TiffWriter::new(&mut bytes);
value.write(&mut writer)?;
}
self.ifd.insert(
tag.to_u16(),
DirectoryEntry {
data_type: <T>::FIELD_TYPE.to_u16(),
let entry = Self::write_value(
self.writer,
&DirectoryEntry {
data_type: <T>::FIELD_TYPE,
count: value.count().try_into()?,
data: bytes,
},
);
)?;
self.directory.extend([(tag, entry)]);
Ok(())
}
fn write_directory(&mut self) -> TiffResult<u64> {
// Start by writing out all values
for &mut DirectoryEntry {
data: ref mut bytes,
..
} in self.ifd.values_mut()
{
let data_bytes = mem::size_of::<K::OffsetType>();
if bytes.len() > data_bytes {
let offset = self.writer.offset();
self.writer.write_bytes(bytes)?;
*bytes = vec![0; data_bytes];
let mut writer = TiffWriter::new(bytes as &mut [u8]);
K::write_offset(&mut writer, offset)?;
} else {
while bytes.len() < data_bytes {
bytes.push(0);
}
}
}
let offset = self.writer.offset();
K::write_entry_count(self.writer, self.ifd.len())?;
for (
tag,
DirectoryEntry {
data_type: field_type,
count,
data: offset,
},
) in self.ifd.iter()
{
self.writer.write_u16(*tag)?;
self.writer.write_u16(*field_type)?;
(*count).write(self.writer)?;
self.writer.write_bytes(offset)?;
}
Ok(offset)
}
/// Write some data to the tiff file, the offset of the data is returned.
///
/// This could be used to write tiff strips.
@@ -293,28 +318,124 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(offset)
}
/// Define the parent directory.
///
/// Each directory has an offset based link to its successor, forming a linked list in the
/// file. The encoder writes its own offset into the parent's field when [`Self::finish`] is
/// called or it is dropped. This redefines the parent. An offset representation of the parent
/// must be acquired by finishing it with [`Self::finish_with_offsets`].
pub fn set_parent(&mut self, offset: &DirectoryOffset<K>) {
self.chained_ifd_pos = Some(offset.ifd_chain);
}
/// Write out the ifd directory itself.
pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal()?;
Ok(())
}
pub fn finish_with_offsets(mut self) -> TiffResult<DirectoryOffset<K>> {
self.finish_internal()
}
fn write_directory(&mut self) -> TiffResult<u64> {
// Start by turning all buffered unwritten values into entries.
let offset = self.writer.offset();
K::write_entry_count(self.writer, self.directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in self.directory.iter() {
self.writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?;
count.write(self.writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
}
Ok(offset)
}
fn write_value(
writer: &mut TiffWriter<W>,
value: &DirectoryEntry<K::OffsetType>,
) -> TiffResult<Entry> {
let &DirectoryEntry {
data: ref bytes,
ref count,
data_type,
} = value;
let in_entry_bytes = mem::size_of::<K::OffsetType>();
let mut offset_bytes = [0; 8];
if bytes.len() > in_entry_bytes {
let offset = writer.offset();
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);
}
// Undoing some hidden type. Offset is either u32 or u64. Due to the trait API being public
// and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it.
// Instead, write it to a buffer...
let mut count_bytes = [0; 8];
// Nominally Cow but we only expect `Cow::Borrowed`.
count_bytes[..count.bytes()].copy_from_slice(&count.data());
Ok(if in_entry_bytes == 4 {
let count = u32::from_ne_bytes(count_bytes[..4].try_into().unwrap());
Entry::new(data_type, count, offset_bytes[..4].try_into().unwrap())
} else {
debug_assert_eq!(in_entry_bytes, 8);
let count = u64::from_ne_bytes(count_bytes);
Entry::new_u64(data_type, count, offset_bytes)
})
}
/// Provides the number of bytes written by the underlying TiffWriter during the last call.
fn last_written(&self) -> u64 {
self.writer.last_written()
}
fn finish_internal(&mut self) -> TiffResult<()> {
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
let ifd_pointer = self.write_directory()?;
let curr_pos = self.writer.offset();
let offset = K::convert_offset(ifd_pointer)?;
if let Some(prior) = self.chained_ifd_pos {
let curr_pos = self.writer.offset();
self.writer.goto_offset(prior.get())?;
// Note how we are not writing the `offset` type itself here as doing so would need to
// go through the `TiffValue` trait—the type is not constrained by much. But for the
// trait we need a `TiffWriter` which comes with a bunch of additional state such as
// compressor etc. that we have no need for.
K::write_offset(self.writer, ifd_pointer)?;
self.writer.goto_offset(curr_pos)?;
}
self.writer.goto_offset(self.ifd_pointer_pos)?;
K::write_offset(self.writer, ifd_pointer)?;
self.writer.goto_offset(curr_pos)?;
K::write_offset(self.writer, 0)?;
let ifd_chain = NonZeroU64::new(self.writer.previous_ifd_pointer::<K>())
.expect("IFD chain field is at a non-zero offset");
if let Some(prior) = self.write_chain.take() {
*prior = ifd_chain;
}
self.dropped = true;
Ok(())
}
/// Write out the ifd directory.
pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal()
Ok(DirectoryOffset {
pointer: IfdPointer(ifd_pointer),
offset,
ifd_chain,
kind: PhantomData,
})
}
}
@@ -411,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)?;
@@ -584,13 +705,13 @@ 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(())
}
fn finish_internal(&mut self) -> TiffResult<()> {
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
self.encoder
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
self.encoder.write_tag(
@@ -609,7 +730,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
/// Write out image and ifd directory.
pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal()
self.finish_internal()?;
Ok(())
}
}
@@ -622,7 +744,7 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W
}
struct DirectoryEntry<S> {
data_type: u16,
data_type: Type,
count: S,
data: Vec<u8>,
}
+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;
+4
View File
@@ -69,6 +69,10 @@ impl<W: Write> TiffWriter<W> {
self.offset
}
pub(crate) fn previous_ifd_pointer<K: super::TiffKind>(&self) -> u64 {
self.offset() - core::mem::size_of::<K::OffsetType>() as u64
}
pub fn last_written(&self) -> u64 {
self.byte_count
}
+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}")
}
}
}
+2
View File
@@ -8,10 +8,12 @@
mod bytecast;
pub mod decoder;
mod directory;
pub mod encoder;
mod error;
pub mod tags;
pub use self::directory::Directory;
pub use self::error::{TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
/// An enumeration over supported color types and their bit depths
+41 -11
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
@@ -78,7 +80,6 @@ pub enum Tag(u16) unknown("A private or extension tag") {
// palette-color images (PhotometricInterpretation 3)
ColorMap = 320, // TODO add support
Compression = 259, // TODO add support for 2 and 32773
Copyright = 33_432,
DateTime = 306,
ExtraSamples = 338, // TODO add support
FillOrder = 266, // TODO add support
@@ -114,6 +115,7 @@ pub enum Tag(u16) unknown("A private or extension tag") {
TileLength = 323,
TileOffsets = 324,
TileByteCounts = 325,
SubIfd = 330,
// Data Sample Format
SampleFormat = 339,
SMinSampleValue = 340, // TODO add support
@@ -124,13 +126,35 @@ pub enum Tag(u16) unknown("A private or extension tag") {
ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group)
ModelTiepointTag = 33922, // (Intergraph)
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
// *Do note its typo in the Decimal id*
Copyright = 33_432,
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
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)
ExifVersion = 0x9000,
GdalNodata = 42113, // Contains areas with missing data
}
}
/// Identifies the offset of an IFD.
///
/// This is represented as a 64-bit integer but only BigTIFF can utilize the bits. It is encoded
/// as 32-bit unsigned value ([`Type::LONG`]) in regular TIFF files and as 64-bit unsigned value
/// ([`Type::IFD8`]) in BigTIFF files.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
// We could be using `NonZeroU64` here but I find that this complicates semantics. This type
// represents the integer value stored as a value in a tag. The semantics of treating `0` as an end
// marker are imposed by the IFD. (It's unclear if Pointer tags such as Exif would allow `0` but in
// practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64);
tags! {
/// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) {
@@ -172,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,
@@ -234,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,
+4
View File
@@ -0,0 +1,4 @@
subsubifd.tif:
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
license: CC0
+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,
);
}
+57 -3
View File
@@ -57,7 +57,7 @@ fn encode_decode() {
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(8)),
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
@@ -137,7 +137,7 @@ fn encode_decode_big() {
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(16)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
@@ -621,8 +621,62 @@ 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:?}"),
}
}
}
}
#[test]
fn test_auxiliary_directory() {
const ARTIST: &str = "Auxiliary-tiff";
let mut data = Cursor::new(Vec::new());
let mut tiff = TiffEncoder::new(&mut data).unwrap();
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(1, 1).unwrap();
image_encoder.write_strip(&[1]).unwrap();
}
let exif = {
let mut extra = tiff.extra_directory().unwrap();
extra.write_tag(Tag::Artist, ARTIST).unwrap();
extra.finish_with_offsets().unwrap()
};
{
let mut image_encoder = tiff.new_image::<colortype::Gray8>(2, 1).unwrap();
image_encoder.write_strip(&[1, 2]).unwrap();
let encoder = image_encoder.encoder();
encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap();
}
data.set_position(0);
let mut decoder = Decoder::new(&mut data).unwrap();
{
assert_eq!(decoder.dimensions().unwrap(), (1, 1));
let _ = decoder.read_image().unwrap();
}
{
decoder.next_image().unwrap();
assert_eq!(decoder.dimensions().unwrap(), (2, 1));
let _ = decoder.read_image().unwrap();
let exif_location = decoder
.get_tag(Tag::ExifDirectory)
.expect("second directory missing Exif")
.into_ifd_pointer()
.expect("exif directory is not an IFD pointer");
let exif = decoder.read_directory(exif_location).unwrap();
let artist = decoder
.read_directory_tags(&exif)
.get_tag_ascii_string(Tag::Artist)
.expect("EXIF directory missing artist tag");
assert_eq!(artist, ARTIST);
}
}
+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;
+169
View File
@@ -0,0 +1,169 @@
use tiff::{
decoder::Decoder,
tags::{IfdPointer, Tag},
};
// This file has the following IFD structure:
//
// ```
// IFD -> IFD -> IFD
// |-> IFD
// |-> IFD -> IFD
// | \-> IFD -> IFD
// \-> IFD
// ```
//
// where the main chain of IFD has 3 images, the second of which has several sub-IFDs and some have
// more nested IFDs.
const TEST_IMAGE_SUBIFD: &str = "./tests/subsubifds.tif";
use std::fs::File;
#[test]
fn decode_seek_chain() {
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");
assert!(!decoder.more_images());
// Ensure we can seek, to do this again.
decoder
.seek_to_image(0)
.expect("Failed to seek to 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());
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]);
}
Binary file not shown.