Compare commits

..
95 Commits
Author SHA1 Message Date
Aurelia Molzer cbcf6b97d4 Bump version number 0.10.1 2025-08-25 23:21:34 +02:00
Sergey "Shnatsel" Davidoff 19c811e86d populate changelog for 0.10.1 2025-08-25 15:20:04 +01:00
Aurelia Molzer 50be15e356 Merge pull request #290 from jakubadamw/nightly-compilation-warnings
Address compilation warnings in the Nightly channel of Rust
2025-08-14 16:50:22 +00:00
Jacob Adam 718f2cb455 Address compilation warnings in the Nightly channel of Rust. 2025-08-14 17:45:21 +01: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
Jonathan Behrens 46086e2bcf Release 0.10.0 (#271) 2025-06-20 20:15:42 -07:00
Jonathan Behrens d7be2a7b67 Make the Compressor enum non-exhaustive (#272) 2025-06-15 18:31:03 -07:00
Emma 29b71ff597 add CMYKA (#208) 2025-06-15 16:38:22 -07:00
Jonathan Behrens 83e6d64751 Create feature flags for more compression algorithms (#270) 2025-06-15 15:50:14 -07:00
Jonathan Behrens dc2c67469b Switch to zune-jpeg (#268)
]
2025-06-14 21:13:16 -07:00
Jonathan Behrens b5d8ee5f86 Switch to quick_error and cleaner error messages (#269) 2025-06-09 19:10:12 -07:00
Jonathan Behrens f44da4a00d Switch from SmartReader to EndianReader (#233) 2025-06-08 18:45:10 -07:00
Wei Ji ee0dbf373b Read Byte as u8, SignedByte as i8 (#236) 2025-06-08 18:31:51 -07:00
Wei Ji aeab243a96 Read Short as u16, SignedShort as i16 (#235) 2025-06-08 18:03:08 -07:00
Wei Ji 36f7b3b4aa Bump MSRV from 1.61.0 to 1.70.0
Found using `cargo msrv find`, the `1.70.0` version is determined by `half=2.4.1` having an MSRV of 1.70.0.
2025-03-16 21:31:12 +00:00
Micah Chambers 3bfb43e83e Add FP16 support (#257) 2025-02-04 19:12:01 -08:00
Jonathan Behrens d42e6be507 Fix formatting (#263) 2025-01-30 23:29:29 -08:00
Stefan Schake a19cf37587 Support ZSTD compression method for reading (#256) 2024-12-20 12:50:34 -08:00
Gunnar Schulze ff52aea06e Add support for iterating over all tags (#255) 2024-11-25 19:34:11 -08:00
Jonathan Behrens 96d3a02efe Fix 1bpp image decoding and add a test image (#252) 2024-10-31 20:21:02 -07:00
Jean-Baptiste Skutnik e28ad564d9 Add horizontal predictor support (#240) 2024-09-23 19:45:45 +01:00
Even Rouault de6d9cf131 README.md: advertize JPEG decode support (#244) 2024-09-07 10:49:22 -07:00
Jonathan Behrens 6dc7a266d3 Make decoding internally operate on byte buffers (#230) 2024-07-22 20:13:39 -07:00
Wei Ji 307cd8be97 Add SignedByte and SignedShort enum variants (#234) 2024-07-21 20:37:25 -07:00
Wei Jiandtromper 0c54a18e21 Add ColorType::Multiband enum variant to support samples>1 per pixel (#224)
Co-authored-by: tromper <scott@hopkinsunderground.com>
2024-03-24 15:04:52 -07:00
Jonathan Behrens 90ae5b8e54 Fix warnings (#225) 2024-03-23 13:51:28 -07:00
Jonathan Behrens 9b06e3726d Sample format should not be a Vec (#226) 2024-03-23 13:51:19 -07:00
Jonathan Behrens 895c70a1ad Test minimal versions in CI (#223) 2024-02-11 12:40:11 -08:00
Jonathan Behrens b0ba4a6788 Release 0.9.1 (#220) 2024-01-13 12:01:04 -08:00
Jonathan Behrens 4a2e76858b Allow arbitrary number of samples as long as all have the same bit depth (#215) 2023-10-04 09:45:03 -07:00
Jonathan Behrens a67f50568c Update edition and CI script, and add Cargo.lock.msrv (#212)
* Add Cargo.lock.msrv, update edition, and CI script

* Also run clippy and verify msrv

* Fix string mismatch

* Have clippy only check for serious errors
2023-08-20 13:44:35 -07:00
Jonathan Behrens d6f6b8a01e Merge pull request #213 from fintelia/fix-clippy
A bunch of clippy fixes
2023-08-20 13:39:27 -07:00
Jonathan Behrens 8ec91b64f6 Cargo fmt 2023-08-20 13:33:35 -07:00
Jonathan Behrens 60f866c119 A bunch of clippy fixes 2023-08-20 13:32:08 -07:00
Jonathan Behrens daa0e3eabf Merge pull request #206 from fintelia/simplify-jpeg
Simplify and avoid allocation in jpeg decompression
2023-06-14 14:04:01 -07:00
Jonathan Behrens 24d203c7c3 Simplify and avoid allocation in jpeg decompression 2023-06-11 12:54:30 -07:00
Jonathan Behrens 473868a7bb Merge pull request #199 from pka/planar-config
Basic support for planar configuration
2023-06-10 12:46:20 -07:00
Andreas Molzer aefea7786f Merge pull request #203 from AlanRace/custom-compression
Don't panic when parsing metadata when custom compression is used
2023-03-08 09:33:03 +01:00
Alan Race d1ad9b2664 Don't panic when parsing metadata when custom compression is used 2023-03-07 09:15:24 +01:00
Andreas Molzer bd1d085189 Merge pull request #201 from image-rs/release-0.8.2
Add changelog and metadata for 0.9.0
2023-02-06 23:58:59 +01:00
Andreas Molzer fcfcecb2df Add changelog and metadata for 0.8.2 2023-02-06 23:37:12 +01:00
Andreas Molzer aa10d27a28 Merge pull request #200 from anfedotoff/master
Fix capacity overflow in JpegReader
2023-02-06 21:22:13 +01:00
fedotoff 8fa175c371 Fix capacity overflow in JpegReader 2023-02-05 17:53:37 +03:00
Pirmin Kalberer 2a15aa6b7e Support also tiled planar images 2023-01-17 11:57:54 +01:00
Pirmin Kalberer 42feeba8da Add basic support for planar configuration 2023-01-12 23:34:31 +01:00
48 changed files with 3550 additions and 1572 deletions

No files matched your search

+73 -19
View File
@@ -1,40 +1,94 @@
name: Rust CI
on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.61", stable, beta, nightly]
command: [build, test]
rust: ["1.74.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v2
- run: rustup default ${{ matrix.rust }}
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.74.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.74.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: build
run: >
cargo build --verbose --no-default-features --features "$FEATURES"
run: cargo build -v
- name: test
run: >
cargo test --tests --benches --no-default-features --features "$FEATURES"
if: ${{ matrix.rust != '1.61' }}
env:
FEATURES: ${{ matrix.features }}
if: ${{ matrix.rust != '1.74.0' }}
run: cargo test -v && cargo doc -v
features:
runs-on: ubuntu-latest
strategy:
matrix:
feature: ["", deflate, jpeg, lzw, zstd, default, "default,zstd"]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- 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:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
override: true
components: rustfmt
- name: Run rustfmt check
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
run: cargo fmt -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --all-features # TODO: enable "-- -D warnings" once all outstanding clippy lints are fixed
+59
View File
@@ -1,3 +1,62 @@
# Version 0.10.1
New features:
- Added a dedicated `Directory` type (#277)
- Expose methods related to `Directory` publicly (#278)
- Implement writing directories (#281, #283)
- Add ICC (color profile) to the set of known tags (#287)
- Added `Decoder.inner()` to access the underlying reader (#289)
- Added methods for reading into a pre-allocated byte buffer (#285)
Changes:
- No longer perform color conversion when decoding embedded JPEG. This fixes decoding for JPEG images with alpha channel. (#274)
- More lenient LZW decoding to match libtiff (#231)
Fixes:
- Fixed seeking multiple times in a row (#279)
# Version 0.10.0
New features:
- Added `ColorType::Multiband` and `ColorType::CMYKA`.
- Added `Value::Byte` and `Value::SignedByte` to represent `u8` and `i8` tags.
- Encoding can now use horizontal predictor.
- The `Decoder::tag_iter` method allows iterating over all tags in the image.
- Half precision floating point values are now supported during decoding.
- ZStandard compression has been added as an additional compression method
during decoding, but requires the `zstd` feature flag.
Changes:
- Switched `u16` and `i16` values to be intepreted as `Value::Short` and
`Value::SignedShort` respectively.
- Changed JPEG compression to use the `zune-jpeg` crate instead of
`jpeg-decoder`.
- The `deflate`, `jpeg`, and `lzw` compression methods are now controlled by
(enabled by default) feature flags.
Fixes:
- Fixed a bug in 1 bit-per-pixel decoding.
# Version 0.9.1
New features:
- Basic support for planar configuration.
- Allow arbitrary number of samples as long as all have the same bit depth.
Fixes:
- Don't panic when parsing metadata when custom compression is used.
# Version 0.9.0
New features:
* Added support for photometric interpretation `YCbCr` and added related
`ColorType`.
Fixes:
* Decoding tiled images calculates padding correctly when image width or height
is a multiple of tile size. It could previously corrupt the last tile per row
by skipping over data.
# Version 0.8.1
Changes:
+21 -6
View File
@@ -1,11 +1,11 @@
[package]
name = "tiff"
version = "0.8.1"
edition = "2018"
version = "0.10.1"
edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.61.0"
rust-version = "1.74.0"
license = "MIT"
description = "TIFF decoding and encoding library in pure Rust"
@@ -17,12 +17,27 @@ categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*"]
[dependencies]
weezl = "0.1.0"
jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false }
flate2 = "1.0.20"
half = { version = "2.4.1" }
quick-error = "2.0.1"
flate2 = { version = "1.0.20", 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.5"
[features]
default = ["deflate", "jpeg", "lzw"]
# Compression algorithms
deflate = ["dep:flate2"]
jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"]
zstd = ["dep:zstd"]
[[bench]]
name = "lzw"
+4 -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)
@@ -24,6 +24,7 @@ This table lists photometric interpretations and sample formats which are suppor
| `RGBPalette` | | |
| `Mask` | | |
| `CMYK` | CMYK(8\|16\|32\|64) | CMYK(32\|64) |
| `CMYKA` | CMYKA(8) | |
| `YCbCr` | | |
| `CIELab` | | |
@@ -35,6 +36,8 @@ This table lists photometric interpretations and sample formats which are suppor
| LZW | ✓ | ✓ |
| Deflate | ✓ | ✓ |
| PackBits | ✓ | ✓ |
| JPEG | ✓ | not yet |
| ZSTD | ✓ | not yet |
## Not yet supported
+6
View File
@@ -10,8 +10,13 @@
//! 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;
macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
pub(crate) fn $const(slice: &[$int]) -> &[u8] {
assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
@@ -31,4 +36,5 @@ integral_slice_as_bytes!(i32, i32_as_ne_bytes, i32_as_ne_mut_bytes);
integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
integral_slice_as_bytes!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
+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");
}
+121 -76
View File
@@ -1,18 +1,17 @@
//! Function for reading TIFF tags
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::io::{self, Read, Seek};
use std::mem;
use std::str;
use super::stream::{ByteOrder, EndianReader, SmartReader};
use crate::tags::{Tag, Type};
use super::stream::{ByteOrder, EndianReader};
use crate::tags::{IfdPointer, Tag, Type};
use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
Short, Signed, SignedBig, Unsigned, UnsignedBig,
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
};
#[allow(unused_qualifications)]
@@ -21,6 +20,8 @@ use self::Value::{
pub enum Value {
Byte(u8),
Short(u16),
SignedByte(i8),
SignedShort(i16),
Signed(i32),
SignedBig(i64),
Unsigned(u32),
@@ -41,91 +42,117 @@ impl Value {
pub fn into_u8(self) -> TiffResult<u8> {
match self {
Byte(val) => Ok(val),
val => Err(TiffError::FormatError(TiffFormatError::ByteExpected(val))),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_i8(self) -> TiffResult<i8> {
match self {
SignedByte(val) => Ok(val),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_u16(self) -> TiffResult<u16> {
match self {
Byte(val) => Ok(val.into()),
Short(val) => Ok(val),
Unsigned(val) => Ok(u16::try_from(val)?),
UnsignedBig(val) => Ok(u16::try_from(val)?),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_i16(self) -> TiffResult<i16> {
match self {
SignedByte(val) => Ok(val.into()),
SignedShort(val) => Ok(val),
Signed(val) => Ok(i16::try_from(val)?),
SignedBig(val) => Ok(i16::try_from(val)?),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_u32(self) -> TiffResult<u32> {
match self {
Byte(val) => Ok(val.into()),
Short(val) => Ok(val.into()),
Unsigned(val) => Ok(val),
UnsignedBig(val) => Ok(u32::try_from(val)?),
Ifd(val) => Ok(val),
IfdBig(val) => Ok(u32::try_from(val)?),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_i32(self) -> TiffResult<i32> {
match self {
SignedByte(val) => Ok(val.into()),
SignedShort(val) => Ok(val.into()),
Signed(val) => Ok(val),
SignedBig(val) => Ok(i32::try_from(val)?),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_u64(self) -> TiffResult<u64> {
match self {
Byte(val) => Ok(val.into()),
Short(val) => Ok(val.into()),
Unsigned(val) => Ok(val.into()),
UnsignedBig(val) => Ok(val),
Ifd(val) => Ok(val.into()),
IfdBig(val) => Ok(val),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_i64(self) -> TiffResult<i64> {
match self {
SignedByte(val) => Ok(val.into()),
SignedShort(val) => Ok(val.into()),
Signed(val) => Ok(val.into()),
SignedBig(val) => Ok(val),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_f32(self) -> TiffResult<f32> {
match self {
Float(val) => Ok(val),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
pub fn into_f64(self) -> TiffResult<f64> {
match self {
Double(val) => Ok(val),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
/// 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),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -138,6 +165,8 @@ impl Value {
}
Ok(new_vec)
}
Byte(val) => Ok(vec![val.into()]),
Short(val) => Ok(vec![val.into()]),
Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
@@ -147,9 +176,7 @@ impl Value {
Ifd(val) => Ok(vec![val]),
IfdBig(val) => Ok(vec![u32::try_from(val)?]),
Ascii(val) => Ok(val.chars().map(u32::from).collect()),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -164,9 +191,7 @@ impl Value {
}
Byte(val) => Ok(vec![val]),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -179,10 +204,9 @@ impl Value {
}
Ok(new_vec)
}
Byte(val) => Ok(vec![val.into()]),
Short(val) => Ok(vec![val]),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -205,15 +229,15 @@ impl Value {
}
Ok(new_vec)
}
SignedByte(val) => Ok(vec![val.into()]),
SignedShort(val) => Ok(vec![val.into()]),
Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
SRationalBig(numerator, denominator) => {
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
}
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -227,9 +251,7 @@ impl Value {
Ok(new_vec)
}
Float(val) => Ok(vec![val]),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -243,9 +265,7 @@ impl Value {
Ok(new_vec)
}
Double(val) => Ok(vec![val]),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -258,6 +278,8 @@ impl Value {
}
Ok(new_vec)
}
Byte(val) => Ok(vec![val.into()]),
Short(val) => Ok(vec![val.into()]),
Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
@@ -265,9 +287,7 @@ impl Value {
Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
val => Err(TiffError::FormatError(
TiffFormatError::UnsignedIntegerExpected(val),
)),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
@@ -290,15 +310,26 @@ impl Value {
}
Ok(new_vec)
}
SignedByte(val) => Ok(vec![val.into()]),
SignedShort(val) => Ok(vec![val.into()]),
Signed(val) => Ok(vec![val.into()]),
SignedBig(val) => Ok(vec![val]),
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
val => Err(TiffError::FormatError(
TiffFormatError::SignedIntegerExpected(val),
)),
_ => 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)]
@@ -319,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 {
@@ -332,23 +363,35 @@ impl Entry {
}
}
/// Returns a mem_reader for the offset/value field
fn r(&self, byte_order: ByteOrder) -> SmartReader<io::Cursor<Vec<u8>>> {
SmartReader::wrap(io::Cursor::new(self.offset.to_vec()), byte_order)
pub fn field_type(&self) -> Type {
self.type_
}
pub fn val<R: Read + Seek>(
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)
}
pub(crate) fn val<R: Read + Seek>(
&self,
limits: &super::Limits,
bigtiff: bool,
reader: &mut SmartReader<R>,
reader: &mut EndianReader<R>,
) -> TiffResult<Value> {
// Case 1: there are no values so we can return immediately.
if self.count == 0 {
return Ok(List(Vec::new()));
}
let bo = reader.byte_order();
let bo = reader.byte_order;
let tag_size = match self.type_ {
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
@@ -401,11 +444,11 @@ 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 => Unsigned(u32::from(self.offset[0])),
Type::SBYTE => Signed(i32::from(self.offset[0] as i8)),
Type::BYTE => Byte(self.offset[0]),
Type::SBYTE => SignedByte(self.offset[0] as i8),
Type::UNDEFINED => Byte(self.offset[0]),
Type::SHORT => Unsigned(u32::from(self.r(bo).read_u16()?)),
Type::SSHORT => Signed(i32::from(self.r(bo).read_i16()?)),
Type::SHORT => Short(self.r(bo).read_u16()?),
Type::SSHORT => SignedShort(self.r(bo).read_i16()?),
Type::LONG => Unsigned(self.r(bo).read_u32()?),
Type::SLONG => Signed(self.r(bo).read_i32()?),
Type::FLOAT => Float(self.r(bo).read_f32()?),
@@ -451,7 +494,7 @@ impl Entry {
Type::SBYTE => return offset_to_sbytes(self.count as usize, self),
Type::ASCII => {
let mut buf = vec![0; self.count as usize];
self.r(bo).read_exact(&mut buf)?;
buf.copy_from_slice(&self.offset[..self.count as usize]);
if buf.is_ascii() && buf.ends_with(&[0]) {
let v = str::from_utf8(&buf)?;
let v = v.trim_matches(char::from(0));
@@ -480,7 +523,7 @@ impl Entry {
let mut r = self.r(bo);
let mut v = Vec::new();
for _ in 0..self.count {
v.push(Signed(i32::from(r.read_i16()?)));
v.push(SignedShort(r.read_i16()?));
}
return Ok(List(v));
}
@@ -533,17 +576,17 @@ impl Entry {
// at a different endianess of file/computer.
Type::BYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
let mut buf = [0; 1];
reader.read_exact(&mut buf)?;
reader.inner().read_exact(&mut buf)?;
Ok(UnsignedBig(u64::from(buf[0])))
}),
Type::SBYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SignedBig(i64::from(reader.read_i8()? as i8)))
Ok(SignedBig(i64::from(reader.read_i8()?)))
}),
Type::SHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(UnsignedBig(u64::from(reader.read_u16()?)))
Ok(Short(reader.read_u16()?))
}),
Type::SSHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(SignedBig(i64::from(reader.read_i16()?)))
Ok(SignedShort(reader.read_i16()?))
}),
Type::LONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
Ok(Unsigned(reader.read_u32()?))
@@ -582,7 +625,7 @@ impl Entry {
Type::UNDEFINED => {
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
let mut buf = [0; 1];
reader.read_exact(&mut buf)?;
reader.inner().read_exact(&mut buf)?;
Ok(Byte(buf[0]))
})
}
@@ -599,7 +642,7 @@ impl Entry {
}
let mut out = vec![0; n];
reader.read_exact(&mut out)?;
reader.inner().read_exact(&mut out)?;
// Strings may be null-terminated, so we trim anything downstream of the null byte
if let Some(first) = out.iter().position(|&b| b == 0) {
out.truncate(first);
@@ -616,12 +659,12 @@ impl Entry {
bo: ByteOrder,
bigtiff: bool,
limits: &super::Limits,
reader: &mut SmartReader<R>,
reader: &mut EndianReader<R>,
decode_fn: F,
) -> TiffResult<Value>
where
R: Read + Seek,
F: Fn(&mut SmartReader<R>) -> TiffResult<Value>,
F: Fn(&mut EndianReader<R>) -> TiffResult<Value>,
{
let value_count = usize::try_from(value_count)?;
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
@@ -667,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>;
+273 -151
View File
@@ -1,12 +1,16 @@
use super::ifd::{Directory, Value};
use super::stream::{ByteOrder, DeflateReader, JpegReader, LZWReader, PackBitsReader};
use super::ifd::Value;
use super::stream::PackBitsReader;
use super::tag_reader::TagReader;
use super::{fp_predict_f32, fp_predict_f64, DecodingBuffer, Limits};
use super::{stream::SmartReader, ChunkType};
use crate::tags::{CompressionMethod, PhotometricInterpretation, Predictor, SampleFormat, Tag};
use crate::{ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError};
use std::convert::{TryFrom, TryInto};
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
};
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)]
@@ -26,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
@@ -62,15 +66,15 @@ pub(crate) struct Image {
pub ifd: Option<Directory>,
pub width: u32,
pub height: u32,
pub bits_per_sample: Vec<u8>,
#[allow(unused)]
pub samples: u8,
pub sample_format: Vec<SampleFormat>,
pub bits_per_sample: u8,
pub samples: u16,
pub sample_format: SampleFormat,
pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod,
pub predictor: Predictor,
pub jpeg_tables: Option<Arc<Vec<u8>>>,
pub chunk_type: ChunkType,
pub planar_config: PlanarConfiguration,
pub strip_decoder: Option<StripDecodeState>,
pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>,
@@ -79,17 +83,10 @@ pub(crate) struct Image {
impl Image {
pub fn from_reader<R: Read + Seek>(
reader: &mut SmartReader<R>,
decoder: &mut ValueReader<R>,
ifd: Directory,
limits: &Limits,
bigtiff: bool,
) -> TiffResult<Image> {
let mut tag_reader = TagReader {
reader,
limits,
ifd: &ifd,
bigtiff,
};
let mut tag_reader = TagReader { decoder, ifd: &ifd };
let width = tag_reader.require_tag(Tag::ImageWidth)?.into_u32()?;
let height = tag_reader.require_tag(Tag::ImageLength)?.into_u32()?;
@@ -109,13 +106,12 @@ impl Image {
// Try to parse both the compression method and the number, format, and bits of the included samples.
// If they are not explicitly specified, those tags are reset to their default values and not carried from previous images.
let compression_method = match tag_reader.find_tag(Tag::Compression)? {
Some(val) => CompressionMethod::from_u16(val.into_u16()?)
.ok_or(TiffUnsupportedError::UnknownCompressionMethod)?,
Some(val) => CompressionMethod::from_u16_exhaustive(val.into_u16()?),
None => CompressionMethod::None,
};
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)?
@@ -132,12 +128,14 @@ impl Image {
None
};
let samples = tag_reader
let samples: u16 = tag_reader
.find_tag(Tag::SamplesPerPixel)?
.map(Value::into_u16)
.transpose()?
.unwrap_or(1)
.try_into()?;
.unwrap_or(1);
if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
}
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => {
@@ -151,17 +149,28 @@ impl Image {
return Err(TiffUnsupportedError::UnsupportedSampleFormat(sample_format).into());
}
sample_format
sample_format[0]
}
None => vec![SampleFormat::Uint],
None => SampleFormat::Uint,
};
let bits_per_sample = match samples {
1 | 3 | 4 => tag_reader
.find_tag_uint_vec(Tag::BitsPerSample)?
.unwrap_or_else(|| vec![1]),
_ => return Err(TiffUnsupportedError::UnsupportedSampleDepth(samples).into()),
};
let bits_per_sample: Vec<u8> = tag_reader
.find_tag_uint_vec(Tag::BitsPerSample)?
.unwrap_or_else(|| vec![1]);
// Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows
// it to be a single value that applies to all samples.
if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
// This library (and libtiff) do not support mixed sample formats and zero bits per sample
// doesn't make sense.
if bits_per_sample.iter().any(|&b| b != bits_per_sample[0]) || bits_per_sample[0] == 0 {
return Err(TiffUnsupportedError::InconsistentBitsPerSample(bits_per_sample).into());
}
let predictor = tag_reader
.find_tag(Tag::Predictor)?
@@ -174,16 +183,33 @@ impl Image {
.transpose()?
.unwrap_or(Predictor::None);
let planar_config = tag_reader
.find_tag(Tag::PlanarConfiguration)?
.map(Value::into_u16)
.transpose()?
.map(|p| {
PlanarConfiguration::from_u16(p).ok_or(TiffError::FormatError(
TiffFormatError::UnknownPlanarConfiguration(p),
))
})
.transpose()?
.unwrap_or(PlanarConfiguration::Chunky);
let planes = match planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples,
};
let chunk_type;
let chunk_offsets;
let chunk_bytes;
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;
@@ -207,7 +233,7 @@ impl Image {
if chunk_offsets.len() != chunk_bytes.len()
|| rows_per_strip == 0
|| u32::try_from(chunk_offsets.len())?
!= height.saturating_sub(1) / rows_per_strip + 1
!= (height.saturating_sub(1) / rows_per_strip + 1) * planes as u32
{
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
@@ -246,7 +272,8 @@ impl Image {
let tile = tile_attributes.as_ref().unwrap();
if chunk_offsets.len() != chunk_bytes.len()
|| chunk_offsets.len() != tile.tiles_down() * tile.tiles_across()
|| chunk_offsets.len()
!= tile.tiles_down() * tile.tiles_across() * planes as usize
{
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
@@ -264,7 +291,7 @@ impl Image {
ifd: Some(ifd),
width,
height,
bits_per_sample,
bits_per_sample: bits_per_sample[0],
samples,
sample_format,
photometric_interpretation,
@@ -272,6 +299,7 @@ impl Image {
jpeg_tables,
predictor,
chunk_type,
planar_config,
strip_decoder,
tile_attributes,
chunk_offsets,
@@ -281,9 +309,9 @@ impl Image {
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.bits_per_sample[..] {
[r, g, b] if [r, r] == [g, b] => Ok(ColorType::RGB(r)),
[r, g, b, a] if [r, r, r] == [g, b, a] => Ok(ColorType::RGBA(r)),
PhotometricInterpretation::RGB => match self.samples {
3 => Ok(ColorType::RGB(self.bits_per_sample)),
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
// FIXME: We should _ignore_ other components. In particular:
// > Beware of extra components. Some TIFF files may have more components per pixel
// than you think. A Baseline TIFF reader must skip over them gracefully,using the
@@ -292,98 +320,134 @@ impl Image {
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::CMYK => match self.bits_per_sample[..] {
[c, m, y, k] if [c, c, c] == [m, y, k] => Ok(ColorType::CMYK(c)),
PhotometricInterpretation::CMYK => match self.samples {
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::YCbCr => match self.bits_per_sample[..] {
[y, cb, cr] if [y, y] == [cb, cr] => Ok(ColorType::YCbCr(y)),
PhotometricInterpretation::YCbCr => match self.samples {
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero
if self.bits_per_sample.len() == 1 =>
{
Ok(ColorType::Gray(self.bits_per_sample[0]))
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
match self.samples {
1 => Ok(ColorType::Gray(self.bits_per_sample)),
_ => Ok(ColorType::Multiband {
bit_depth: self.bits_per_sample,
num_samples: self.samples,
}),
}
}
// TODO: this is bad we should not fail at this point
_ => Err(TiffError::UnsupportedError(
PhotometricInterpretation::RGBPalette
| PhotometricInterpretation::TransparencyMask
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
self.bits_per_sample.clone(),
vec![self.bits_per_sample; self.samples as usize],
),
)),
}
}
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,
photometric_interpretation: PhotometricInterpretation,
compression_method: CompressionMethod,
compressed_length: u64,
jpeg_tables: Option<Arc<Vec<u8>>>,
#[allow(unused_variables)] jpeg_tables: Option<&[u8]>,
) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method {
CompressionMethod::None => Box::new(reader),
CompressionMethod::LZW => {
Box::new(LZWReader::new(reader, usize::try_from(compressed_length)?))
}
#[cfg(feature = "lzw")]
CompressionMethod::LZW => Box::new(super::stream::LZWReader::new(
reader,
usize::try_from(compressed_length)?,
)),
#[cfg(feature = "zstd")]
CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
#[cfg(feature = "deflate")]
CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
Box::new(DeflateReader::new(reader))
Box::new(super::stream::DeflateReader::new(reader))
}
#[cfg(feature = "jpeg")]
CompressionMethod::ModernJPEG => {
use zune_jpeg::zune_core;
if jpeg_tables.is_some() && compressed_length < 2 {
return Err(TiffError::FormatError(
TiffFormatError::InvalidTagValueType(Tag::JPEGTables),
));
}
let jpeg_reader = JpegReader::new(reader, compressed_length, jpeg_tables)?;
let mut decoder = jpeg::Decoder::new(jpeg_reader);
// Construct new jpeg_reader wrapping a SmartReader.
//
// JPEG compression in TIFF allows saving quantization and/or huffman tables in one
// central location. These `jpeg_tables` are simply prepended to the remaining jpeg image data.
// Because these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker
// which is also at the beginning of the remaining JPEG image data and would
// confuse the JPEG renderer, one of these has to be taken off. In this case the first two
// bytes of the remaining JPEG data is removed because it follows `jpeg_tables`.
// Similary, `jpeg_tables` ends with a `EOI` (HEX: `0xFFD9`) or __end of image__ marker,
// this has to be removed as well (last two bytes of `jpeg_tables`).
let mut jpeg_reader = match jpeg_tables {
Some(jpeg_tables) => {
let mut reader = reader.take(compressed_length);
reader.read_exact(&mut [0; 2])?;
match photometric_interpretation {
PhotometricInterpretation::RGB => {
decoder.set_color_transform(jpeg::ColorTransform::RGB)
}
PhotometricInterpretation::WhiteIsZero => {
decoder.set_color_transform(jpeg::ColorTransform::None)
}
PhotometricInterpretation::BlackIsZero => {
decoder.set_color_transform(jpeg::ColorTransform::None)
}
PhotometricInterpretation::TransparencyMask => {
decoder.set_color_transform(jpeg::ColorTransform::None)
}
PhotometricInterpretation::CMYK => {
decoder.set_color_transform(jpeg::ColorTransform::CMYK)
}
PhotometricInterpretation::YCbCr => {
decoder.set_color_transform(jpeg::ColorTransform::YCbCr)
}
photometric_interpretation => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedInterpretation(
photometric_interpretation,
),
));
Box::new(
Cursor::new(&jpeg_tables[..jpeg_tables.len() - 2])
.chain(reader.take(compressed_length)),
) as Box<dyn Read>
}
None => Box::new(reader.take(compressed_length)),
};
let mut jpeg_data = Vec::new();
jpeg_reader.read_to_end(&mut jpeg_data)?;
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg_data);
let mut options: zune_core::options::DecoderOptions = Default::default();
// 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()?;
Box::new(Cursor::new(data))
@@ -396,6 +460,28 @@ impl Image {
})
}
/// Samples per pixel within chunk.
///
/// In planar config, samples are stored in separate strips/chunks, also called bands.
///
/// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`:
/// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...)
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...)
pub(crate) fn samples_per_pixel(&self) -> usize {
match self.planar_config {
PlanarConfiguration::Chunky => self.samples.into(),
PlanarConfiguration::Planar => 1,
}
}
/// Number of strips per pixel.
pub(crate) fn strips_per_pixel(&self) -> usize {
match self.planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.samples.into(),
}
}
pub(crate) fn chunk_file_range(&self, chunk: u32) -> TiffResult<(u64, u64)> {
let file_offset = self
.chunk_offsets
@@ -435,7 +521,10 @@ impl Image {
match self.chunk_type {
ChunkType::Strip => {
let strip_height_without_padding = chunk_index
let strip_attrs = self.strip_decoder.as_ref().unwrap();
let strips_per_band =
self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1;
let strip_height_without_padding = (chunk_index % strips_per_band)
.checked_mul(dims.1)
.and_then(|x| self.height.checked_sub(x))
.ok_or(TiffError::UsageError(UsageError::InvalidChunkIndex(
@@ -461,27 +550,43 @@ impl Image {
pub(crate) fn expand_chunk(
&self,
reader: impl Read,
mut buffer: DecodingBuffer,
output_width: usize,
byte_order: ByteOrder,
reader: &mut ValueReader<impl Read>,
buf: &mut [u8],
output_row_stride: usize,
chunk_index: u32,
) -> TiffResult<()> {
// Validate that the provided buffer is of the expected type.
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, &buffer) {
(ColorType::RGB(n), _)
| (ColorType::RGBA(n), _)
| (ColorType::CMYK(n), _)
| (ColorType::YCbCr(n), _)
| (ColorType::Gray(n), _)
if usize::from(n) == buffer.byte_len() * 8 => {}
(ColorType::Gray(n), DecodingBuffer::U8(_)) if n < 8 => match self.predictor {
match color_type {
ColorType::RGB(n)
| ColorType::RGBA(n)
| ColorType::CMYK(n)
| ColorType::CMYKA(n)
| ColorType::YCbCr(n)
| ColorType::Gray(n)
| ColorType::Multiband {
bit_depth: n,
num_samples: _,
} if n == 8 || n == 16 || n == 32 || n == 64 => {}
ColorType::Gray(n)
| ColorType::Multiband {
bit_depth: n,
num_samples: _,
} if n < 8 => match self.predictor {
Predictor::None => {}
Predictor::Horizontal => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::HorizontalPredictor(color_type),
))
));
}
Predictor::FloatingPoint => {
return Err(TiffError::UnsupportedError(
@@ -489,23 +594,25 @@ impl Image {
));
}
},
(type_, _) => {
type_ => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedColorType(type_),
))
));
}
}
// Validate that the predictor is supported for the sample type.
match (self.predictor, &buffer) {
(Predictor::Horizontal, DecodingBuffer::F32(_))
| (Predictor::Horizontal, DecodingBuffer::F64(_)) => {
match (self.predictor, self.sample_format) {
(
Predictor::Horizontal,
SampleFormat::Int | SampleFormat::Uint | SampleFormat::IEEEFP,
) => {}
(Predictor::Horizontal, _) => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::HorizontalPredictor(color_type),
));
}
(Predictor::FloatingPoint, DecodingBuffer::F32(_))
| (Predictor::FloatingPoint, DecodingBuffer::F64(_)) => {}
(Predictor::FloatingPoint, SampleFormat::IEEEFP) => {}
(Predictor::FloatingPoint, _) => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::FloatingPointPredictor(color_type),
@@ -520,78 +627,93 @@ impl Image {
.ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
return Err(TiffError::LimitsExceeded);
}
let byte_len = buffer.byte_len();
let compression_method = self.compression_method;
let photometric_interpretation = self.photometric_interpretation;
let predictor = self.predictor;
let samples = self.bits_per_sample.len();
let samples = self.samples_per_pixel();
let chunk_dims = self.chunk_dimensions()?;
let data_dims = self.chunk_data_dimensions(chunk_index)?;
let padding_right = chunk_dims.0 - data_dims.0;
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.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.div_ceil(8).try_into()?;
// TODO: Should these return errors instead?
assert!(output_row_stride >= data_row_bytes);
assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
let jpeg_tables = self.jpeg_tables.clone();
let mut reader = Self::create_reader(
reader,
photometric_interpretation,
compression_method,
*compressed_bytes,
jpeg_tables,
self.jpeg_tables.as_deref().map(|a| &**a),
)?;
if output_width == data_dims.0 as usize && padding_right == 0 {
let total_samples = data_dims.0 as usize * data_dims.1 as usize * samples;
let tile = &mut buffer.as_bytes_mut()[..total_samples * byte_len];
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 0..data_dims.1 as usize {
let row_start = row as usize * output_width as usize * samples;
let row_end = (row + 1) * output_width as usize * samples;
let row = buffer.subrange(row_start..row_end);
super::fix_endianness_and_predict(row, samples, byte_order, predictor);
for row in tile.chunks_mut(chunk_row_bytes) {
super::fix_endianness_and_predict(
row,
color_type.bit_depth(),
samples,
byte_order,
predictor,
);
}
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(&mut buffer.subrange(0..total_samples), color_type);
super::invert_colors(tile, color_type, self.sample_format);
}
} else if padding_right > 0 && self.predictor == Predictor::FloatingPoint {
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
// The floating point predictor shuffles the padding bytes into the encoded output, so
// this case is handled specially when needed.
let mut encoded = vec![0u8; chunk_dims.0 as usize * samples * byte_len];
for row in 0..data_dims.1 as usize {
let row_start = row * output_width as usize * samples;
let row_end = row_start + data_dims.0 as usize * samples;
let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut encoded)?;
match buffer.subrange(row_start..row_end) {
DecodingBuffer::F32(buf) => fp_predict_f32(&mut encoded, buf, samples),
DecodingBuffer::F64(buf) => fp_predict_f64(&mut encoded, buf, samples),
let row = &mut row[..data_row_bytes];
match color_type.bit_depth() {
16 => predict_f16(&mut encoded, row, samples),
32 => predict_f32(&mut encoded, row, samples),
64 => predict_f64(&mut encoded, row, samples),
_ => unreachable!(),
}
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(&mut buffer.subrange(row_start..row_end), color_type);
super::invert_colors(row, color_type, self.sample_format);
}
}
} else {
for row in 0..data_dims.1 as usize {
let row_start = row * output_width as usize * samples;
let row_end = row_start + data_dims.0 as usize * samples;
let row = &mut buffer.as_bytes_mut()[(row_start * byte_len)..(row_end * byte_len)];
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)?;
// Skip horizontal padding
if padding_right > 0 {
let len = u64::try_from(padding_right as usize * samples * byte_len)?;
if chunk_row_bytes > data_row_bytes {
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
io::copy(&mut reader.by_ref().take(len), &mut io::sink())?;
}
let mut row = buffer.subrange(row_start..row_end);
super::fix_endianness_and_predict(row.copy(), samples, byte_order, predictor);
super::fix_endianness_and_predict(
row,
color_type.bit_depth(),
samples,
byte_order,
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(&mut row, color_type);
super::invert_colors(row, color_type, self.sample_format);
}
}
}
+867 -491
View File
File diff suppressed because it is too large. Load diff
+71 -203
View File
@@ -1,8 +1,6 @@
//! All IO functionality needed for TIFF decoding
use std::convert::TryFrom;
use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom, Take};
use std::sync::Arc;
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug)]
@@ -14,16 +12,35 @@ pub enum ByteOrder {
}
/// Reader that is aware of the byte order.
pub trait EndianReader: Read {
/// Byte order that should be adhered to
fn byte_order(&self) -> ByteOrder;
#[derive(Debug)]
pub struct EndianReader<R> {
reader: R,
pub(crate) byte_order: ByteOrder,
}
impl<R: Read> EndianReader<R> {
pub fn new(reader: R, byte_order: ByteOrder) -> Self {
Self { reader, byte_order }
}
pub fn inner(&mut self) -> &mut R {
&mut self.reader
}
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()>
where
R: Seek,
{
self.reader.seek(io::SeekFrom::Start(offset))?;
Ok(())
}
/// Reads an u16
#[inline(always)]
fn read_u16(&mut self) -> Result<u16, io::Error> {
pub fn read_u16(&mut self) -> Result<u16, io::Error> {
let mut n = [0u8; 2];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes(n),
ByteOrder::BigEndian => u16::from_be_bytes(n),
})
@@ -31,10 +48,10 @@ pub trait EndianReader: Read {
/// Reads an i8
#[inline(always)]
fn read_i8(&mut self) -> Result<i8, io::Error> {
pub fn read_i8(&mut self) -> Result<i8, io::Error> {
let mut n = [0u8; 1];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => i8::from_le_bytes(n),
ByteOrder::BigEndian => i8::from_be_bytes(n),
})
@@ -42,10 +59,10 @@ pub trait EndianReader: Read {
/// Reads an i16
#[inline(always)]
fn read_i16(&mut self) -> Result<i16, io::Error> {
pub fn read_i16(&mut self) -> Result<i16, io::Error> {
let mut n = [0u8; 2];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => i16::from_le_bytes(n),
ByteOrder::BigEndian => i16::from_be_bytes(n),
})
@@ -53,10 +70,10 @@ pub trait EndianReader: Read {
/// Reads an u32
#[inline(always)]
fn read_u32(&mut self) -> Result<u32, io::Error> {
pub fn read_u32(&mut self) -> Result<u32, io::Error> {
let mut n = [0u8; 4];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n),
})
@@ -64,10 +81,10 @@ pub trait EndianReader: Read {
/// Reads an i32
#[inline(always)]
fn read_i32(&mut self) -> Result<i32, io::Error> {
pub fn read_i32(&mut self) -> Result<i32, io::Error> {
let mut n = [0u8; 4];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => i32::from_le_bytes(n),
ByteOrder::BigEndian => i32::from_be_bytes(n),
})
@@ -75,10 +92,10 @@ pub trait EndianReader: Read {
/// Reads an u64
#[inline(always)]
fn read_u64(&mut self) -> Result<u64, io::Error> {
pub fn read_u64(&mut self) -> Result<u64, io::Error> {
let mut n = [0u8; 8];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n),
})
@@ -86,10 +103,10 @@ pub trait EndianReader: Read {
/// Reads an i64
#[inline(always)]
fn read_i64(&mut self) -> Result<i64, io::Error> {
pub fn read_i64(&mut self) -> Result<i64, io::Error> {
let mut n = [0u8; 8];
self.read_exact(&mut n)?;
Ok(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(match self.byte_order {
ByteOrder::LittleEndian => i64::from_le_bytes(n),
ByteOrder::BigEndian => i64::from_be_bytes(n),
})
@@ -97,10 +114,10 @@ pub trait EndianReader: Read {
/// Reads an f32
#[inline(always)]
fn read_f32(&mut self) -> Result<f32, io::Error> {
pub fn read_f32(&mut self) -> Result<f32, io::Error> {
let mut n = [0u8; 4];
self.read_exact(&mut n)?;
Ok(f32::from_bits(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(f32::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes(n),
ByteOrder::BigEndian => u32::from_be_bytes(n),
}))
@@ -108,49 +125,53 @@ pub trait EndianReader: Read {
/// Reads an f64
#[inline(always)]
fn read_f64(&mut self) -> Result<f64, io::Error> {
pub fn read_f64(&mut self) -> Result<f64, io::Error> {
let mut n = [0u8; 8];
self.read_exact(&mut n)?;
Ok(f64::from_bits(match self.byte_order() {
self.reader.read_exact(&mut n)?;
Ok(f64::from_bits(match self.byte_order {
ByteOrder::LittleEndian => u64::from_le_bytes(n),
ByteOrder::BigEndian => u64::from_be_bytes(n),
}))
}
}
///
/// # 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")]
pub struct LZWReader<R: Read> {
reader: BufReader<Take<R>>,
decoder: weezl::decode::Decoder,
}
#[cfg(feature = "lzw")]
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(),
}
}
}
#[cfg(feature = "lzw")]
impl<R: Read> Read for LZWReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
@@ -183,108 +204,11 @@ impl<R: Read> Read for LZWReader<R> {
}
}
///
/// ## JPEG Reader (for "new-style" JPEG format (TIFF compression tag 7))
///
pub(crate) struct JpegReader {
jpeg_tables: Option<Arc<Vec<u8>>>,
buffer: io::Cursor<Vec<u8>>,
offset: usize,
}
impl JpegReader {
/// Constructs new JpegReader wrapping a SmartReader.
/// Because JPEG compression in TIFF allows to save quantization and/or huffman tables in one
/// central location, the constructor accepts this data as `jpeg_tables` here containing either
/// or both.
/// These `jpeg_tables` are simply prepended to the remaining jpeg image data.
/// Because these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker
/// which is also at the beginning of the remaining JPEG image data and would
/// confuse the JPEG renderer, one of these has to be taken off. In this case the first two
/// bytes of the remaining JPEG data is removed because it follows `jpeg_tables`.
/// Similary, `jpeg_tables` ends with a `EOI` (HEX: `0xFFD9`) or __end of image__ marker,
/// this has to be removed as well (last two bytes of `jpeg_tables`).
pub fn new<R: Read>(
mut reader: R,
length: u64,
jpeg_tables: Option<Arc<Vec<u8>>>,
) -> io::Result<JpegReader> {
// Read jpeg image data
let mut segment = vec![0; length as usize];
reader.read_exact(&mut segment[..])?;
match jpeg_tables {
Some(jpeg_tables) => {
assert!(
jpeg_tables.len() >= 2,
"jpeg_tables, if given, must be at least 2 bytes long. Got {:?}",
jpeg_tables
);
assert!(
length >= 2,
"if jpeg_tables is given, length must be at least 2 bytes long, got {}",
length
);
let mut buffer = io::Cursor::new(segment);
// Skip the first two bytes (marker bytes)
buffer.seek(SeekFrom::Start(2))?;
Ok(JpegReader {
buffer,
jpeg_tables: Some(jpeg_tables),
offset: 0,
})
}
None => Ok(JpegReader {
buffer: io::Cursor::new(segment),
jpeg_tables: None,
offset: 0,
}),
}
}
}
impl Read for JpegReader {
// #[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut start = 0;
if let Some(jpeg_tables) = &self.jpeg_tables {
if jpeg_tables.len() - 2 > self.offset {
// Read (rest of) jpeg_tables to buf (without the last two bytes)
let size_remaining = jpeg_tables.len() - self.offset - 2;
let to_copy = size_remaining.min(buf.len());
buf[start..start + to_copy]
.copy_from_slice(&jpeg_tables[self.offset..self.offset + to_copy]);
self.offset += to_copy;
if to_copy == buf.len() {
return Ok(to_copy);
}
start += to_copy;
}
}
let read = self.buffer.read(&mut buf[start..])?;
self.offset += read;
Ok(read + start)
}
}
///
/// ## PackBits Reader
///
//
// ## PackBits Reader
//
/// Internal state machine for the PackBitsReader.
enum PackBitsReaderState {
Header,
Literal,
@@ -318,7 +242,7 @@ impl<R: Read> Read for PackBitsReader<R> {
let mut header: [u8; 1] = [0];
self.reader.read_exact(&mut header)?;
let h = header[0] as i8;
if h >= -127 && h <= -1 {
if (-127..=-1).contains(&h) {
let mut data: [u8; 1] = [0];
self.reader.read_exact(&mut data)?;
self.state = PackBitsReaderState::Repeat { value: data[0] };
@@ -348,63 +272,7 @@ impl<R: Read> Read for PackBitsReader<R> {
if self.count == 0 {
self.state = PackBitsReaderState::Header;
}
return Ok(actual);
}
}
///
/// ## SmartReader Reader
///
/// Reader that is aware of the byte order.
#[derive(Debug)]
pub struct SmartReader<R>
where
R: Read,
{
reader: R,
pub byte_order: ByteOrder,
}
impl<R> SmartReader<R>
where
R: Read,
{
/// Wraps a reader
pub fn wrap(reader: R, byte_order: ByteOrder) -> SmartReader<R> {
SmartReader { reader, byte_order }
}
pub fn into_inner(self) -> R {
self.reader
}
}
impl<R: Read + Seek> SmartReader<R> {
pub fn goto_offset(&mut self, offset: u64) -> io::Result<()> {
self.seek(io::SeekFrom::Start(offset)).map(|_| ())
}
}
impl<R> EndianReader for SmartReader<R>
where
R: Read,
{
#[inline(always)]
fn byte_order(&self) -> ByteOrder {
self.byte_order
}
}
impl<R: Read> Read for SmartReader<R> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}
impl<R: Read + Seek> Seek for SmartReader<R> {
#[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.reader.seek(pos)
Ok(actual)
}
}
+28 -16
View File
@@ -1,26 +1,37 @@
use std::convert::TryFrom;
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::SmartReader;
use super::Limits;
use super::ifd::{Entry, Value};
use super::ValueReader;
pub(crate) struct TagReader<'a, R: Read + Seek> {
pub reader: &'a mut SmartReader<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),
@@ -29,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 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"
);
}
}
+108
View File
@@ -1,5 +1,31 @@
use crate::tags::{PhotometricInterpretation, SampleFormat};
macro_rules! integer_horizontal_predict {
() => {
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>) {
let sample_size = Self::SAMPLE_FORMAT.len();
if row.len() < sample_size {
debug_assert!(false);
return;
}
let (start, rest) = row.split_at(sample_size);
result.extend_from_slice(start);
if result.capacity() - result.len() < rest.len() {
return;
}
result.extend(
row.into_iter()
.zip(rest)
.map(|(prev, current)| current.wrapping_sub(*prev)),
);
}
};
}
/// Trait for different colortypes that can be encoded.
pub trait ColorType {
/// The type of each sample of this colortype
@@ -10,6 +36,8 @@ pub trait ColorType {
const BITS_PER_SAMPLE: &'static [u16];
/// The value of the tiff tag `SampleFormat`
const SAMPLE_FORMAT: &'static [SampleFormat];
fn horizontal_predict(row: &[Self::Inner], result: &mut Vec<Self::Inner>);
}
pub struct Gray8;
@@ -18,6 +46,8 @@ impl ColorType for Gray8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI8;
@@ -26,6 +56,8 @@ impl ColorType for GrayI8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
}
pub struct Gray16;
@@ -34,6 +66,8 @@ impl ColorType for Gray16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI16;
@@ -42,6 +76,8 @@ impl ColorType for GrayI16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
}
pub struct Gray32;
@@ -50,6 +86,8 @@ impl ColorType for Gray32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI32;
@@ -58,6 +96,8 @@ impl ColorType for GrayI32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
}
pub struct Gray32Float;
@@ -66,6 +106,10 @@ impl ColorType for Gray32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct Gray64;
@@ -74,6 +118,8 @@ impl ColorType for Gray64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint];
integer_horizontal_predict!();
}
pub struct GrayI64;
@@ -82,6 +128,8 @@ impl ColorType for GrayI64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Int];
integer_horizontal_predict!();
}
pub struct Gray64Float;
@@ -90,6 +138,10 @@ impl ColorType for Gray64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::BlackIsZero;
const BITS_PER_SAMPLE: &'static [u16] = &[64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct RGB8;
@@ -98,6 +150,8 @@ impl ColorType for RGB8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct RGB16;
@@ -106,6 +160,8 @@ impl ColorType for RGB16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct RGB32;
@@ -114,6 +170,8 @@ impl ColorType for RGB32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct RGB32Float;
@@ -122,6 +180,9 @@ impl ColorType for RGB32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct RGB64;
@@ -130,6 +191,8 @@ impl ColorType for RGB64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct RGB64Float;
@@ -138,6 +201,9 @@ impl ColorType for RGB64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct RGBA8;
@@ -146,6 +212,8 @@ impl ColorType for RGBA8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct RGBA16;
@@ -154,6 +222,8 @@ impl ColorType for RGBA16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct RGBA32;
@@ -162,6 +232,8 @@ impl ColorType for RGBA32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct RGBA32Float;
@@ -170,6 +242,9 @@ impl ColorType for RGBA32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct RGBA64;
@@ -178,6 +253,8 @@ impl ColorType for RGBA64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct RGBA64Float;
@@ -186,6 +263,9 @@ impl ColorType for RGBA64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct CMYK8;
@@ -194,6 +274,8 @@ impl ColorType for CMYK8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct CMYK16;
@@ -202,6 +284,8 @@ impl ColorType for CMYK16 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[16, 16, 16, 16];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct CMYK32;
@@ -210,6 +294,8 @@ impl ColorType for CMYK32 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct CMYK32Float;
@@ -218,6 +304,10 @@ impl ColorType for CMYK32Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct CMYK64;
@@ -226,6 +316,8 @@ impl ColorType for CMYK64 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 4];
integer_horizontal_predict!();
}
pub struct CMYK64Float;
@@ -234,6 +326,10 @@ impl ColorType for CMYK64Float {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4];
fn horizontal_predict(_: &[Self::Inner], _: &mut Vec<Self::Inner>) {
unreachable!()
}
}
pub struct YCbCr8;
@@ -242,4 +338,16 @@ impl ColorType for YCbCr8 {
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::YCbCr;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 3];
integer_horizontal_predict!();
}
pub struct CMYKA8;
impl ColorType for CMYKA8 {
type Inner = u8;
const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::CMYK;
const BITS_PER_SAMPLE: &'static [u16] = &[8, 8, 8, 8, 8];
const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::Uint; 5];
integer_horizontal_predict!();
}
+4 -9
View File
@@ -1,6 +1,5 @@
use crate::{encoder::compression::*, tags::CompressionMethod};
use crate::encoder::compression::*;
use flate2::{write::ZlibEncoder, Compression as FlateCompression};
use std::io::Write;
/// The Deflate algorithm used to compress image data in TIFF files.
#[derive(Debug, Clone, Copy)]
@@ -12,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 {
@@ -46,7 +41,7 @@ impl Compression for Deflate {
const COMPRESSION_METHOD: CompressionMethod = CompressionMethod::Deflate;
fn get_algorithm(&self) -> Compressor {
Compressor::Deflate(self.clone())
Compressor::Deflate(*self)
}
}
+2 -3
View File
@@ -1,5 +1,4 @@
use crate::{encoder::compression::*, tags::CompressionMethod};
use std::io::Write;
use crate::encoder::compression::*;
use weezl::encode::Encoder as LZWEncoder;
/// The LZW algorithm used to compress image data in TIFF files.
@@ -41,7 +40,7 @@ mod tests {
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Lzw::default().write_to(&mut writer, TEST_DATA).unwrap();
Lzw.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(EXPECTED_COMPRESSED_DATA, compressed_data.as_slice());
}
}
+13 -3
View File
@@ -1,13 +1,19 @@
use crate::tags::CompressionMethod;
use std::io::{self, Write};
#[cfg(feature = "deflate")]
mod deflate;
#[cfg(feature = "lzw")]
mod lzw;
mod packbits;
mod uncompressed;
#[cfg(feature = "deflate")]
pub use self::deflate::{Deflate, DeflateLevel};
#[cfg(feature = "lzw")]
pub use self::lzw::Lzw;
pub use self::packbits::Packbits;
pub use self::uncompressed::Uncompressed;
@@ -28,9 +34,12 @@ pub trait Compression: CompressionAlgorithm {
}
/// An enum to store each compression algorithm.
#[non_exhaustive]
pub enum Compressor {
Uncompressed(Uncompressed),
#[cfg(feature = "lzw")]
Lzw(Lzw),
#[cfg(feature = "deflate")]
Deflate(Deflate),
Packbits(Packbits),
}
@@ -38,7 +47,7 @@ pub enum Compressor {
impl Default for Compressor {
/// The default compression strategy does not apply any compression.
fn default() -> Self {
Compressor::Uncompressed(Uncompressed::default())
Compressor::Uncompressed(Uncompressed)
}
}
@@ -46,7 +55,9 @@ impl CompressionAlgorithm for Compressor {
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
match self {
Compressor::Uncompressed(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "lzw")]
Compressor::Lzw(algorithm) => algorithm.write_to(writer, bytes),
#[cfg(feature = "deflate")]
Compressor::Deflate(algorithm) => algorithm.write_to(writer, bytes),
Compressor::Packbits(algorithm) => algorithm.write_to(writer, bytes),
}
@@ -55,6 +66,5 @@ impl CompressionAlgorithm for Compressor {
#[cfg(test)]
mod tests {
pub const TEST_DATA: &'static [u8] =
b"This is a string for checking various compression algorithms.";
pub const TEST_DATA: &[u8] = b"This is a string for checking various compression algorithms.";
}
+32 -42
View File
@@ -1,5 +1,5 @@
use crate::{encoder::compression::*, tags::CompressionMethod};
use std::io::{BufWriter, Error, ErrorKind, Write};
use crate::encoder::compression::*;
use std::io::{BufWriter, Error, ErrorKind};
/// Compressor that uses the Packbits[^note] algorithm to compress bytes.
///
@@ -80,33 +80,31 @@ impl CompressionAlgorithm for Packbits {
run_index = 0;
in_run = false;
}
} else {
if bytes_pending > MAX_BYTES {
// We have as much differing data as we can output in one chunk.
// Output MAX_BYTES leaving one byte.
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(MAX_BYTES))?);
bufwriter.write(&bytes[pending_index..pending_index + MAX_BYTES as usize])?;
bytes_written += 1 + MAX_BYTES as u64;
} else if bytes_pending > MAX_BYTES {
// We have as much differing data as we can output in one chunk.
// Output MAX_BYTES leaving one byte.
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(MAX_BYTES))?);
bufwriter.write_all(&bytes[pending_index..pending_index + MAX_BYTES as usize])?;
bytes_written += 1 + MAX_BYTES as u64;
pending_index += MAX_BYTES as usize;
bytes_pending -= MAX_BYTES;
run_index = bytes_pending - 1; // A run could start here
} else if curr_byte == last_byte {
if (bytes_pending - run_index >= MIN_REPT) || (run_index == 0) {
// This is a worthwhile run
if run_index != 0 {
// Flush differing data out of input buffer
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?);
bufwriter
.write(&bytes[pending_index..pending_index + run_index as usize])?;
bytes_written += 1 + run_index as u64;
}
bytes_pending -= run_index; // Length of run
in_run = true;
pending_index += MAX_BYTES as usize;
bytes_pending -= MAX_BYTES;
run_index = bytes_pending - 1; // A run could start here
} else if curr_byte == last_byte {
if (bytes_pending - run_index >= MIN_REPT) || (run_index == 0) {
// This is a worthwhile run
if run_index != 0 {
// Flush differing data out of input buffer
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(run_index))?);
bufwriter
.write_all(&bytes[pending_index..pending_index + run_index as usize])?;
bytes_written += 1 + run_index as u64;
}
} else {
run_index = bytes_pending - 1; // A run could start here
bytes_pending -= run_index; // Length of run
in_run = true;
}
} else {
run_index = bytes_pending - 1; // A run could start here
}
last_byte = curr_byte;
}
@@ -119,7 +117,7 @@ impl CompressionAlgorithm for Packbits {
} else {
bytes_written += 1 + bytes_pending as u64;
offset.get_or_insert(write_u8(&mut bufwriter, encode_diff(bytes_pending))?);
bufwriter.write(&bytes[pending_index..pending_index + bytes_pending as usize])?;
bufwriter.write_all(&bytes[pending_index..pending_index + bytes_pending as usize])?;
}
bufwriter.flush()?;
@@ -141,24 +139,20 @@ mod tests {
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits::default()
.write_to(&mut writer, &UNCOMPRESSED_DATA)
.unwrap();
Packbits.write_to(&mut writer, &UNCOMPRESSED_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
#[test]
fn test_packbits_rept() {
// compress buffer with repetitive sequence
const UNCOMPRESSED_DATA: &'static [u8] =
const UNCOMPRESSED_DATA: &[u8] =
b"This strrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrring hangs.";
const EXPECTED_COMPRESSED_DATA: &'static [u8] = b"\x06This st\xD1r\x09ing hangs.";
const EXPECTED_COMPRESSED_DATA: &[u8] = b"\x06This st\xD1r\x09ing hangs.";
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits::default()
.write_to(&mut writer, UNCOMPRESSED_DATA)
.unwrap();
Packbits.write_to(&mut writer, UNCOMPRESSED_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
@@ -192,23 +186,19 @@ mod tests {
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits::default()
.write_to(&mut writer, data.as_slice())
.unwrap();
Packbits.write_to(&mut writer, data.as_slice()).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
#[test]
fn test_packbits() {
// compress teststring
const EXPECTED_COMPRESSED_DATA: &'static [u8] =
const EXPECTED_COMPRESSED_DATA: &[u8] =
b"\x3CThis is a string for checking various compression algorithms.";
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Packbits::default()
.write_to(&mut writer, TEST_DATA)
.unwrap();
Packbits.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(compressed_data, EXPECTED_COMPRESSED_DATA);
}
}
+2 -5
View File
@@ -1,5 +1,4 @@
use crate::{encoder::compression::*, tags::CompressionMethod};
use std::io::Write;
use crate::encoder::compression::*;
/// The default algorithm which does not compress at all.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -29,9 +28,7 @@ mod tests {
fn test_no_compression() {
let mut compressed_data = Vec::<u8>::new();
let mut writer = Cursor::new(&mut compressed_data);
Uncompressed::default()
.write_to(&mut writer, TEST_DATA)
.unwrap();
Uncompressed.write_to(&mut writer, TEST_DATA).unwrap();
assert_eq!(TEST_DATA, compressed_data);
}
}
+325 -145
View File
@@ -2,18 +2,17 @@ pub use tiff_value::*;
use std::{
cmp,
collections::BTreeMap,
convert::{TryFrom, TryInto},
io::{self, Seek, Write},
marker::PhantomData,
mem,
num::TryFromIntError,
num::{NonZeroU64, TryFromIntError},
};
use crate::{
error::TiffResult,
tags::{CompressionMethod, ResolutionUnit, Tag},
TiffError, TiffFormatError,
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
Directory, TiffError, TiffFormatError,
};
pub mod colortype;
@@ -22,9 +21,62 @@ mod tiff_value;
mod writer;
use self::colortype::*;
use self::compression::Compression as Comp;
use self::compression::*;
use self::writer::*;
/// Type of prediction to prepare the image with.
///
/// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple
/// passes ran over the image data to prepare it for compression. This is mostly used for LZW
/// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression
/// ratio over the unpredicted compression !
///
/// [Predictor::FloatingPoint] is currently not supported.
pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
#[derive(Clone, Copy, PartialEq)]
pub enum Compression {
Uncompressed,
#[cfg(feature = "lzw")]
Lzw,
#[cfg(feature = "deflate")]
Deflate(DeflateLevel),
Packbits,
}
impl Default for Compression {
fn default() -> Self {
Self::Uncompressed
}
}
impl Compression {
fn tag(&self) -> CompressionMethod {
match self {
Compression::Uncompressed => CompressionMethod::None,
#[cfg(feature = "lzw")]
Compression::Lzw => CompressionMethod::LZW,
#[cfg(feature = "deflate")]
Compression::Deflate(_) => CompressionMethod::Deflate,
Compression::Packbits => CompressionMethod::PackBits,
}
}
fn get_algorithm(&self) -> Compressor {
match self {
Compression::Uncompressed => compression::Uncompressed {}.get_algorithm(),
#[cfg(feature = "lzw")]
Compression::Lzw => compression::Lzw {}.get_algorithm(),
#[cfg(feature = "deflate")]
Compression::Deflate(level) => compression::Deflate::with_level(*level).get_algorithm(),
Compression::Packbits => compression::Packbits {}.get_algorithm(),
}
}
}
/// Encoder for Tiff and BigTiff files.
///
/// With this type you can get a `DirectoryEncoder` or a `ImageEncoder`
@@ -53,6 +105,10 @@ use self::writer::*;
pub struct TiffEncoder<W, K: TiffKind = TiffKindStandard> {
writer: TiffWriter<W>,
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.
@@ -81,19 +137,60 @@ 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,
last_ifd_chain,
})
}
K::write_header(&mut encoder.writer)?;
/// Set the predictor to use
///
/// A predictor is used to simplify the file before writing it. This is very
/// useful when writing a file compressed using LZW as it can improve efficiency
pub fn with_predictor(mut self, predictor: Predictor) -> Self {
self.predictor = predictor;
Ok(encoder)
self
}
/// Set the compression method to use
pub fn with_compression(mut self, compression: Compression) -> Self {
self.compression = compression;
self
}
/// 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.
@@ -101,20 +198,9 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
&mut self,
width: u32,
height: u32,
) -> TiffResult<ImageEncoder<W, C, K, Uncompressed>> {
let encoder = DirectoryEncoder::new(&mut self.writer)?;
ImageEncoder::new(encoder, width, height)
}
/// Create an [`ImageEncoder`] to encode an image one slice at a time.
pub fn new_image_with_compression<C: ColorType, D: Compression>(
&mut self,
width: u32,
height: u32,
compression: D,
) -> TiffResult<ImageEncoder<W, C, K, D>> {
let encoder = DirectoryEncoder::new(&mut self.writer)?;
ImageEncoder::with_compression(encoder, width, height, compression)
) -> TiffResult<ImageEncoder<'_, W, C, K>> {
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
}
/// Convenience function to write an entire image from memory.
@@ -127,26 +213,22 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
where
[C::Inner]: TiffValue,
{
let encoder = DirectoryEncoder::new(&mut self.writer)?;
let image: ImageEncoder<W, C, K> = ImageEncoder::new(encoder, width, height)?;
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)
}
/// Convenience function to write an entire image from memory with a given compression.
pub fn write_image_with_compression<C: ColorType, D: Compression>(
&mut self,
width: u32,
height: u32,
compression: D,
data: &[C::Inner],
) -> TiffResult<()>
where
[C::Inner]: TiffValue,
{
let encoder = DirectoryEncoder::new(&mut self.writer)?;
let image: ImageEncoder<W, C, K, D> =
ImageEncoder::with_compression(encoder, width, height, compression)?;
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)
}
}
@@ -156,119 +238,204 @@ 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(&mut self.writer, self.ifd.len())?;
for (
tag,
&DirectoryEntry {
data_type: ref field_type,
ref count,
data: ref offset,
},
) in self.ifd.iter()
{
self.writer.write_u16(*tag)?;
self.writer.write_u16(*field_type)?;
(*count).write(&mut 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.
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
let offset = self.writer.offset();
value.write(&mut self.writer)?;
value.write(self.writer)?;
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)?;
self.writer.goto_offset(self.ifd_pointer_pos)?;
K::write_offset(&mut self.writer, ifd_pointer)?;
self.writer.goto_offset(curr_pos)?;
K::write_offset(&mut self.writer, 0)?;
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)?;
}
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,
})
}
}
@@ -313,13 +480,7 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
/// # }
/// ```
/// You can also call write_data function wich will encode by strip and finish
pub struct ImageEncoder<
'a,
W: 'a + Write + Seek,
C: ColorType,
K: TiffKind,
D: Compression = Uncompressed,
> {
pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
encoder: DirectoryEncoder<'a, W, K>,
strip_idx: u64,
strip_count: u64,
@@ -330,25 +491,30 @@ pub struct ImageEncoder<
strip_offsets: Vec<K::OffsetType>,
strip_byte_count: Vec<K::OffsetType>,
dropped: bool,
compression: D,
compression: Compression,
predictor: Predictor,
_phantom: ::std::marker::PhantomData<C>,
}
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
ImageEncoder<'a, W, T, K, D>
{
fn new(encoder: DirectoryEncoder<'a, W, K>, width: u32, height: u32) -> TiffResult<Self>
where
D: Default,
{
Self::with_compression(encoder, width, height, D::default())
impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> {
fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> {
match (predictor, compression, T::SAMPLE_FORMAT[0]) {
(Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => {
Err(TiffError::UsageError(UsageError::PredictorIncompatible))
}
(Predictor::FloatingPoint, _, _) => {
Err(TiffError::UsageError(UsageError::PredictorUnavailable))
}
_ => Ok(()),
}
}
fn with_compression(
fn new(
mut encoder: DirectoryEncoder<'a, W, K>,
width: u32,
height: u32,
compression: D,
compression: Compression,
predictor: Predictor,
) -> TiffResult<Self> {
if width == 0 || height == 0 {
return Err(TiffError::FormatError(TiffFormatError::InvalidDimensions(
@@ -356,23 +522,26 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
)));
}
Self::sanity_check(compression, predictor)?;
let row_samples = u64::from(width) * u64::try_from(<T>::BITS_PER_SAMPLE.len())?;
let row_bytes = row_samples * u64::from(<T::Inner>::BYTE_LEN);
// Limit the strip size to prevent potential memory and security issues.
// Also keep the multiple strip handling 'oiled'
let rows_per_strip = {
match D::COMPRESSION_METHOD {
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)?;
encoder.write_tag(Tag::Compression, D::COMPRESSION_METHOD.to_u16())?;
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
encoder.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
@@ -400,7 +569,8 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
strip_offsets: Vec::new(),
strip_byte_count: Vec::new(),
dropped: false,
compression: compression,
compression,
predictor,
_phantom: ::std::marker::PhantomData,
})
}
@@ -433,7 +603,18 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
}
// Write the (possible compressed) data to the encoder.
let offset = self.encoder.write_data(value)?;
let offset = match self.predictor {
Predictor::None => self.encoder.write_data(value)?,
Predictor::Horizontal => {
let mut row_result = Vec::with_capacity(value.len());
for row in value.chunks_exact(self.row_samples as usize) {
T::horizontal_predict(row, &mut row_result);
}
self.encoder.write_data(row_result.as_slice())?
}
_ => unimplemented!(),
};
let byte_count = self.encoder.last_written() as usize;
self.strip_offsets.push(K::convert_offset(offset)?);
@@ -524,13 +705,13 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
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(
@@ -549,13 +730,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind, D: Compression>
/// Write out image and ifd directory.
pub fn finish(mut self) -> TiffResult<()> {
self.finish_internal()
self.finish_internal()?;
Ok(())
}
}
impl<'a, W: Write + Seek, C: ColorType, K: TiffKind, D: Compression> Drop
for ImageEncoder<'a, W, C, K, D>
{
impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W, C, K> {
fn drop(&mut self) {
if !self.dropped {
let _ = self.finish_internal();
@@ -564,7 +744,7 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind, D: Compression> Drop
}
struct DirectoryEntry<S> {
data_type: u16,
data_type: Type,
count: S,
data: Vec<u8>,
}
+29 -29
View File
@@ -15,7 +15,7 @@ pub trait TiffValue {
/// Access this value as an contiguous sequence of bytes.
/// If their is no trivial representation, allocate it on the heap.
fn data(&self) -> Cow<[u8]>;
fn data(&self) -> Cow<'_, [u8]>;
/// Write this value to a TiffWriter.
/// While the default implementation will work in all cases, it may require unnecessary allocations.
@@ -34,7 +34,7 @@ impl TiffValue for [u8] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(self)
}
}
@@ -47,7 +47,7 @@ impl TiffValue for [i8] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
}
}
@@ -60,7 +60,7 @@ impl TiffValue for [u16] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
}
}
@@ -73,7 +73,7 @@ impl TiffValue for [i16] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
}
}
@@ -86,7 +86,7 @@ impl TiffValue for [u32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
}
}
@@ -99,7 +99,7 @@ impl TiffValue for [i32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
}
}
@@ -112,7 +112,7 @@ impl TiffValue for [u64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
}
}
@@ -125,7 +125,7 @@ impl TiffValue for [i64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
}
}
@@ -138,7 +138,7 @@ impl TiffValue for [f32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe
Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
}
@@ -152,7 +152,7 @@ impl TiffValue for [f64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe
Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
}
@@ -171,7 +171,7 @@ impl TiffValue for u8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(from_ref(self))
}
}
@@ -189,7 +189,7 @@ impl TiffValue for i8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
}
}
@@ -207,7 +207,7 @@ impl TiffValue for u16 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
}
}
@@ -225,7 +225,7 @@ impl TiffValue for i16 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
}
}
@@ -243,7 +243,7 @@ impl TiffValue for u32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
}
}
@@ -261,7 +261,7 @@ impl TiffValue for i32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
}
}
@@ -279,7 +279,7 @@ impl TiffValue for u64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
}
}
@@ -297,7 +297,7 @@ impl TiffValue for i64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
}
}
@@ -315,7 +315,7 @@ impl TiffValue for f32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
}
}
@@ -333,7 +333,7 @@ impl TiffValue for f64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
}
}
@@ -351,7 +351,7 @@ impl TiffValue for Ifd {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
}
}
@@ -369,7 +369,7 @@ impl TiffValue for Ifd8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
}
}
@@ -388,7 +388,7 @@ impl TiffValue for Rational {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d));
@@ -411,7 +411,7 @@ impl TiffValue for SRational {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d));
@@ -438,7 +438,7 @@ impl TiffValue for str {
}
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
if self.is_ascii() && !self.bytes().any(|b| b == 0) {
let bytes: &[u8] = self.as_bytes();
@@ -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;
@@ -462,7 +462,7 @@ impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
(*self).write(writer)
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
T::data(self)
}
}
@@ -484,7 +484,7 @@ macro_rules! impl_tiff_value_for_contiguous_sequence {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len());
for x in self {
buf.extend_from_slice(&x.data());
+5 -1
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
}
@@ -182,7 +186,7 @@ impl<W: Write> TiffWriter<W> {
impl<W: Seek> TiffWriter<W> {
pub fn goto_offset(&mut self, offset: u64) -> Result<(), io::Error> {
self.offset = offset;
self.writer.seek(SeekFrom::Start(offset as u64))?;
self.writer.seek(SeekFrom::Start(offset))?;
Ok(())
}
}
+175 -318
View File
@@ -1,284 +1,192 @@
use std::error::Error;
use std::fmt;
use std::fmt::Display;
use std::io;
use std::str;
use std::string;
use std::sync::Arc;
use jpeg::UnsupportedFeature;
use quick_error::quick_error;
use crate::decoder::{ifd::Value, ChunkType};
use crate::decoder::ChunkType;
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
};
use crate::ColorType;
use crate::weezl::LzwError;
quick_error! {
/// Tiff error kinds.
#[derive(Debug)]
pub enum TiffError {
/// The Image is not formatted properly.
FormatError(err: TiffFormatError) {
display("format error: {err}")
from()
source(err)
}
/// The Decoder does not support features required by the image.
UnsupportedError(err: TiffUnsupportedError) {
display("unsupported error: {err}")
from()
source(err)
}
/// An I/O Error occurred while decoding the image.
IoError(err: io::Error) {
display("{err}")
from()
source(err)
}
/// The Limits of the Decoder is exceeded.
LimitsExceeded {
display("decoder limits exceeded")
}
/// An integer conversion to or from a platform size failed.
IntSizeError {
display("platform or format size limits exceeded")
}
/// The image does not support the requested operation
UsageError(err: UsageError) {
display("usage error: {err}")
from()
source(err)
}
}
}
/// Tiff error kinds.
#[derive(Debug)]
pub enum TiffError {
/// The Image is not formatted properly.
FormatError(TiffFormatError),
quick_error! {
/// The image is not formatted properly.
///
/// This indicates that the encoder producing the image might behave incorrectly or that the
/// input file has been corrupted.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TiffFormatError {
TiffSignatureNotFound {
display("TIFF signature not found")
}
TiffSignatureInvalid {
display("TIFF signature invalid")
}
ImageFileDirectoryNotFound {
display("image file directory not found")
}
InconsistentSizesEncountered {
display("inconsistent sizes encountered")
}
InvalidDimensions(width: u32, height: u32) {
display("invalid dimensions: {width}x{height}")
}
InvalidTag {
display("image contains invalid tag")
}
InvalidTagValueType(tag: Tag) {
display("tag `{tag:?}` did not have the expected value type")
}
RequiredTagNotFound(tag: Tag) {
display("required tag `{tag:?}` not found")
}
UnknownPredictor(pred: u16) {
display("unknown predictor “{pred}” encountered")
}
UnknownPlanarConfiguration(cfg: u16) {
display("unknown planar configuration “{cfg}”")
}
InvalidTypeForTag {
display("tag has invalid type")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
CycleInOffsets {
display("file contained a cycle in the list of IFDs")
}
SamplesPerPixelIsZero {
display("samples per pixel is zero")
}
CompressedDataCorrupt(message: String) {
display("compressed data is corrupt: {message}")
}
}
}
quick_error! {
/// The Decoder does not support features required by the image.
UnsupportedError(TiffUnsupportedError),
/// An I/O Error occurred while decoding the image.
IoError(io::Error),
/// The Limits of the Decoder is exceeded.
LimitsExceeded,
/// An integer conversion to or from a platform size failed, either due to
/// limits of the platform size or limits of the format.
IntSizeError,
/// The image does not support the requested operation
UsageError(UsageError),
}
/// The image is not formatted properly.
///
/// This indicates that the encoder producing the image might behave incorrectly or that the input
/// file has been corrupted.
///
/// The list of variants may grow to incorporate errors of future features. Matching against this
/// exhaustively is not covered by interface stability guarantees.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TiffFormatError {
TiffSignatureNotFound,
TiffSignatureInvalid,
ImageFileDirectoryNotFound,
InconsistentSizesEncountered,
UnexpectedCompressedData {
actual_bytes: usize,
required_bytes: usize,
},
InconsistentStripSamples {
actual_samples: usize,
required_samples: usize,
},
InvalidDimensions(u32, u32),
InvalidTag,
InvalidTagValueType(Tag),
RequiredTagNotFound(Tag),
UnknownPredictor(u16),
ByteExpected(Value),
UnsignedIntegerExpected(Value),
SignedIntegerExpected(Value),
Format(String),
RequiredTagEmpty(Tag),
StripTileTagConflict,
CycleInOffsets,
JpegDecoder(JpegDecoderError),
}
impl fmt::Display for TiffFormatError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::TiffFormatError::*;
match *self {
TiffSignatureNotFound => write!(fmt, "TIFF signature not found."),
TiffSignatureInvalid => write!(fmt, "TIFF signature invalid."),
ImageFileDirectoryNotFound => write!(fmt, "Image file directory not found."),
InconsistentSizesEncountered => write!(fmt, "Inconsistent sizes encountered."),
UnexpectedCompressedData {
actual_bytes,
required_bytes,
} => {
write!(
fmt,
"Decompression returned different amount of bytes than expected: got {}, expected {}.",
actual_bytes, required_bytes
)
}
InconsistentStripSamples {
actual_samples,
required_samples,
} => {
write!(
fmt,
"Inconsistent elements in strip: got {}, expected {}.",
actual_samples, required_samples
)
}
InvalidDimensions(width, height) => write!(fmt, "Invalid dimensions: {}x{}.", width, height),
InvalidTag => write!(fmt, "Image contains invalid tag."),
InvalidTagValueType(ref tag) => {
write!(fmt, "Tag `{:?}` did not have the expected value type.", tag)
}
RequiredTagNotFound(ref tag) => write!(fmt, "Required tag `{:?}` not found.", tag),
UnknownPredictor(ref predictor) => {
write!(fmt, "Unknown predictor “{}” encountered", predictor)
}
ByteExpected(ref val) => write!(fmt, "Expected byte, {:?} found.", val),
UnsignedIntegerExpected(ref val) => {
write!(fmt, "Expected unsigned integer, {:?} found.", val)
}
SignedIntegerExpected(ref val) => {
write!(fmt, "Expected signed integer, {:?} found.", val)
}
Format(ref val) => write!(fmt, "Invalid format: {:?}.", val),
RequiredTagEmpty(ref val) => write!(fmt, "Required tag {:?} was empty.", val),
StripTileTagConflict => write!(fmt, "File should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found."),
CycleInOffsets => write!(fmt, "File contained a cycle in the list of IFDs"),
JpegDecoder(ref error) => write!(fmt, "{}", error),
///
/// This only captures known failures for which the standard either does not require support or an
/// implementation has been planned but not yet completed. Some variants may become unused over
/// time and will then get deprecated before being removed.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TiffUnsupportedError {
FloatingPointPredictor(color_type: ColorType) {
display("floating point predictor for {color_type:?} is unsupported")
}
HorizontalPredictor(color_type: ColorType) {
display("horizontal predictor for {color_type:?} is unsupported")
}
InconsistentBitsPerSample(bits_per_sample: Vec<u8>) {
display("inconsistent bits per sample: {bits_per_sample:?}")
}
InterpretationWithBits(interpretation: PhotometricInterpretation, bits_per_sample: Vec<u8>) {
display("Photometric interpretation {interpretation:?} with bits per sample {bits_per_sample:?} is unsupported")
}
UnknownInterpretation {
display("unknown photometric interpretation")
}
UnknownCompressionMethod {
display("unknown compression method")
}
UnsupportedCompressionMethod(method: CompressionMethod) {
display("compression method {method:?} is unsupported")
}
UnsupportedSampleDepth(depth: u8) {
display("{depth} samples per pixel is unsupported")
}
UnsupportedSampleFormat(sample_format: Vec<SampleFormat>) {
display("sample format {sample_format:?} is unsupported")
}
UnsupportedColorType(color_type: ColorType) {
display("color type {color_type:?} is unsupported")
}
UnsupportedBitsPerChannel(bits_per_channel: u8) {
display("{bits_per_channel} bits per channel not supported")
}
UnsupportedPlanarConfig(planar: Option<PlanarConfiguration>) {
display("unsupported planar configuration “{planar:?}”")
}
UnsupportedDataType {
display("unsupported data type.")
}
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
display("unsupported photometric interpretation \"{interpretation:?}\"")
}
MisalignedTileBoundaries {
display("tile rows are not aligned to byte boundaries")
}
}
}
/// The Decoder does not support features required by the image.
///
/// This only captures known failures for which the standard either does not require support or an
/// implementation has been planned but not yet completed. Some variants may become unused over
/// time and will then get deprecated before being removed.
///
/// The list of variants may grow. Matching against this exhaustively is not covered by interface
/// stability guarantees.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TiffUnsupportedError {
FloatingPointPredictor(ColorType),
HorizontalPredictor(ColorType),
InterpretationWithBits(PhotometricInterpretation, Vec<u8>),
UnknownInterpretation,
UnknownCompressionMethod,
UnsupportedCompressionMethod(CompressionMethod),
UnsupportedSampleDepth(u8),
UnsupportedSampleFormat(Vec<SampleFormat>),
UnsupportedColorType(ColorType),
UnsupportedBitsPerChannel(u8),
UnsupportedPlanarConfig(Option<PlanarConfiguration>),
UnsupportedDataType,
UnsupportedInterpretation(PhotometricInterpretation),
UnsupportedJpegFeature(UnsupportedFeature),
}
impl fmt::Display for TiffUnsupportedError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::TiffUnsupportedError::*;
match *self {
FloatingPointPredictor(color_type) => write!(
fmt,
"Floating point predictor for {:?} is unsupported.",
color_type
),
HorizontalPredictor(color_type) => write!(
fmt,
"Horizontal predictor for {:?} is unsupported.",
color_type
),
InterpretationWithBits(ref photometric_interpretation, ref bits_per_sample) => write!(
fmt,
"{:?} with {:?} bits per sample is unsupported",
photometric_interpretation, bits_per_sample
),
UnknownInterpretation => write!(
fmt,
"The image is using an unknown photometric interpretation."
),
UnknownCompressionMethod => write!(fmt, "Unknown compression method."),
UnsupportedCompressionMethod(method) => {
write!(fmt, "Compression method {:?} is unsupported", method)
}
UnsupportedSampleDepth(samples) => {
write!(fmt, "{} samples per pixel is unsupported.", samples)
}
UnsupportedSampleFormat(ref formats) => {
write!(fmt, "Sample format {:?} is unsupported.", formats)
}
UnsupportedColorType(color_type) => {
write!(fmt, "Color type {:?} is unsupported", color_type)
}
UnsupportedBitsPerChannel(bits) => {
write!(fmt, "{} bits per channel not supported", bits)
}
UnsupportedPlanarConfig(config) => {
write!(fmt, "Unsupported planar configuration “{:?}”.", config)
}
UnsupportedDataType => write!(fmt, "Unsupported data type."),
UnsupportedInterpretation(interpretation) => {
write!(
fmt,
"Unsupported photometric interpretation \"{:?}\".",
interpretation
)
}
UnsupportedJpegFeature(ref unsupported_feature) => {
write!(fmt, "Unsupported JPEG feature {:?}", unsupported_feature)
}
quick_error! {
/// User attempted to use the Decoder in a way that is incompatible with a specific image.
///
/// For example: attempting to read a tile from a stripped image.
#[derive(Debug)]
#[non_exhaustive]
pub enum UsageError {
InvalidChunkType(expected: ChunkType, actual: ChunkType) {
display("requested operation is only valid for images with chunk encoding of type {expected:?} but got {actual:?}")
}
}
}
/// User attempted to use the Decoder in a way that is incompatible with a specific image.
///
/// For example: attempting to read a tile from a stripped image.
#[derive(Debug)]
pub enum UsageError {
InvalidChunkType(ChunkType, ChunkType),
InvalidChunkIndex(u32),
}
impl fmt::Display for UsageError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::UsageError::*;
match *self {
InvalidChunkType(expected, actual) => {
write!(
fmt,
"Requested operation is only valid for images with chunk encoding of type: {:?}, got {:?}.",
expected, actual
)
}
InvalidChunkIndex(index) => write!(fmt, "Image chunk index ({}) requested.", index),
InvalidChunkIndex(index: u32) {
display("invalid chunk index ({index}) requested")
}
}
}
impl fmt::Display for TiffError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
TiffError::FormatError(ref e) => write!(fmt, "Format error: {}", e),
TiffError::UnsupportedError(ref f) => write!(
fmt,
"The Decoder does not support the \
image format `{}`",
f
),
TiffError::IoError(ref e) => e.fmt(fmt),
TiffError::LimitsExceeded => write!(fmt, "The Decoder limits are exceeded"),
TiffError::IntSizeError => write!(fmt, "Platform or format size limits exceeded"),
TiffError::UsageError(ref e) => write!(fmt, "Usage error: {}", e),
PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
}
}
}
impl Error for TiffError {
fn description(&self) -> &str {
match *self {
TiffError::FormatError(..) => "Format error",
TiffError::UnsupportedError(..) => "Unsupported error",
TiffError::IoError(..) => "IO error",
TiffError::LimitsExceeded => "Decoder limits exceeded",
TiffError::IntSizeError => "Platform or format size limits exceeded",
TiffError::UsageError(..) => "Invalid usage",
PredictorIncompatible {
display("the requested predictor is not compatible with the image's format")
}
}
fn cause(&self) -> Option<&dyn Error> {
match *self {
TiffError::IoError(ref e) => Some(e),
_ => None,
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}")
}
}
}
impl From<io::Error> for TiffError {
fn from(err: io::Error) -> TiffError {
TiffError::IoError(err)
}
}
@@ -294,74 +202,23 @@ impl From<string::FromUtf8Error> for TiffError {
}
}
impl From<TiffFormatError> for TiffError {
fn from(err: TiffFormatError) -> TiffError {
TiffError::FormatError(err)
}
}
impl From<TiffUnsupportedError> for TiffError {
fn from(err: TiffUnsupportedError) -> TiffError {
TiffError::UnsupportedError(err)
}
}
impl From<UsageError> for TiffError {
fn from(err: UsageError) -> TiffError {
TiffError::UsageError(err)
}
}
impl From<std::num::TryFromIntError> for TiffError {
fn from(_err: std::num::TryFromIntError) -> TiffError {
TiffError::IntSizeError
}
}
impl From<LzwError> for TiffError {
fn from(err: LzwError) -> TiffError {
match err {
LzwError::InvalidCode => TiffError::FormatError(TiffFormatError::Format(String::from(
"LZW compressed data corrupted",
))),
}
#[cfg(feature = "lzw")]
impl From<weezl::LzwError> for TiffError {
fn from(err: weezl::LzwError) -> TiffError {
TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct JpegDecoderError {
inner: Arc<jpeg::Error>,
}
impl JpegDecoderError {
fn new(error: jpeg::Error) -> Self {
Self {
inner: Arc::new(error),
}
}
}
impl PartialEq for JpegDecoderError {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
impl Display for JpegDecoderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl From<JpegDecoderError> for TiffError {
fn from(error: JpegDecoderError) -> Self {
TiffError::FormatError(TiffFormatError::JpegDecoder(error))
}
}
impl From<jpeg::Error> for TiffError {
fn from(error: jpeg::Error) -> Self {
JpegDecoderError::new(error).into()
#[cfg(feature = "jpeg")]
impl From<zune_jpeg::errors::DecodeErrors> for TiffError {
fn from(err: zune_jpeg::errors::DecodeErrors) -> Self {
TiffError::FormatError(TiffFormatError::CompressedDataCorrupt(err.to_string()))
}
}
+24 -3
View File
@@ -6,19 +6,19 @@
//! # Related Links
//! * <https://web.archive.org/web/20210108073850/https://www.adobe.io/open/standards/TIFF.html> - The TIFF specification
extern crate jpeg;
extern crate weezl;
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
#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)]
#[non_exhaustive]
pub enum ColorType {
/// Pixel is grayscale
Gray(u8),
@@ -38,6 +38,27 @@ pub enum ColorType {
/// Pixel is CMYK
CMYK(u8),
/// Pixel is CMYK with an alpha channel
CMYKA(u8),
/// Pixel is YCbCr
YCbCr(u8),
/// Pixel has multiple bands/channels
Multiband { bit_depth: u8, num_samples: u16 },
}
impl ColorType {
fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
| ColorType::Palette(b)
| ColorType::GrayA(b)
| ColorType::RGBA(b)
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
}
+50 -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) {
pub enum CompressionMethod(u16) unknown(
/// A custom compression method
unknown
) {
None = 1,
Huffman = 2,
Fax3 = 3,
@@ -184,6 +211,9 @@ pub enum CompressionMethod(u16) {
Deflate = 8,
OldDeflate = 0x80B2,
PackBits = 0x8005,
// Self-assigned by libtiff
ZSTD = 0xC350,
}
}
@@ -209,8 +239,14 @@ pub enum PlanarConfiguration(u16) {
tags! {
pub enum Predictor(u16) {
/// No changes were made to the data
None = 1,
/// The images' rows were processed to contain the difference of each pixel from the previous one.
///
/// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
/// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
Horizontal = 2,
/// Not currently supported
FloatingPoint = 3,
}
}
@@ -225,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
+195
View File
@@ -0,0 +1,195 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
/// Test a basic all white image
#[test]
fn test_white_ieee_fp16() {
let filenames = ["white-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for p in img {
assert!(p == half::f16::from_f32_const(1.0));
}
} else {
panic!("Wrong data type");
}
}
}
/// Test a single black pixel, to make sure scaling is ok
#[test]
fn test_one_black_pixel_ieee_fp16() {
let filenames = ["single-black-fp16.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (i, p) in img.iter().enumerate() {
if i == 0 {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with horizontal differencing predictor
#[test]
fn test_pattern_horizontal_differencing_ieee_fp16() {
let filenames = ["white-fp16-pred2.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test white with floating point predictor
#[test]
fn test_pattern_predictor_ieee_fp16() {
let filenames = ["white-fp16-pred3.tiff"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(256, 256)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
// 0, 2, 5, 8, 12, 16, 255 are black
let black = [0, 2, 5, 8, 12, 16, 255];
for (i, p) in img.iter().enumerate() {
if black.contains(&i) {
assert!(p < &half::f16::from_f32_const(0.001));
} else {
assert!(p == &half::f16::from_f32_const(1.0));
}
}
} else {
panic!("Wrong data type");
}
}
}
/// Test several random images
/// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
#[test]
fn test_predictor_ieee_fp16() {
// first parse pnm, skip the first 4 \n
let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
// PGM looks like this:
// ---
// P5
// #Created with GIMP
// 16 16
// 65535
// ... <big-endian bytes>
// ---
// get index of 4th \n
let byte_start = pnm_bytes
.iter()
.enumerate()
.filter(|(_, &v)| v == b'\n')
.map(|(i, _)| i)
.nth(3)
.expect("Must be 4 \\n's");
let pnm_values: Vec<f32> = pnm_bytes[(byte_start + 1)..]
.chunks(2)
.map(|slice| {
let bts = [slice[0], slice[1]];
(u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
})
.collect();
assert!(pnm_values.len() == 256);
let filenames = [
"random-fp16-pred2.tiff",
"random-fp16-pred3.tiff",
"random-fp16.tiff",
];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
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.dimensions().expect("Cannot get dimensions"),
(16, 16)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Gray(16)
);
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
assert!((exp - found.to_f32()).abs() < 0.0001);
}
} else {
panic!("Wrong data type");
}
}
}
+63
View File
@@ -0,0 +1,63 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
use tiff::ColorType;
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images";
#[test]
fn test_geo_tiff() {
let filenames = ["geo-5b.tif"];
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
assert_eq!(
decoder.dimensions().expect("Cannot get dimensions"),
(10, 10)
);
assert_eq!(
decoder.colortype().expect("Cannot get colortype"),
ColorType::Multiband {
bit_depth: 16,
num_samples: 5
}
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripOffsets)
.expect("Cannot get StripOffsets"),
418
);
assert_eq!(
decoder
.get_tag_u64(Tag::RowsPerStrip)
.expect("Cannot get RowsPerStrip"),
10
);
assert_eq!(
decoder
.get_tag_u64(Tag::StripByteCounts)
.expect("Cannot get StripByteCounts"),
1000
);
assert_eq!(
decoder
.get_tag(Tag::ModelPixelScaleTag)
.expect("Cannot get pixel scale")
.into_f64_vec()
.expect("Cannot get pixel scale"),
vec![60.0, 60.0, 0.0]
);
let DecodingResult::I16(data) = decoder.read_image().unwrap() else {
panic!("Cannot read band data")
};
assert_eq!(data.len(), 500);
}
}
+215 -20
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:?}"),
};
}
}
@@ -216,6 +217,11 @@ fn issue_69() {
//assert!(img_res.is_ok());
//}
#[test]
fn test_tiled_gray_i1() {
test_image_sum_u8("tiled-gray-i1.tif", ColorType::Gray(1), 30531);
}
#[test]
fn test_tiled_rgb_u8() {
test_image_sum_u8("tiled-rgb-u8.tif", ColorType::RGB(8), 39528948);
@@ -226,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);
@@ -285,6 +339,51 @@ 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);
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");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let chunks = decoder.strip_count().unwrap();
assert_eq!(chunks as usize, 72);
// convert -quiet planar-rgb-u8.tif[0] -crop 1x1+0+0 txt:
// 0,0: (73,51,30) #49331E srgb(73,51,30)
// 1st band (red)
match decoder.read_chunk(0).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 73);
}
_ => panic!("Wrong bit depth"),
}
// 2nd band (green)
match decoder.read_chunk(chunks / 3).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 51);
}
_ => panic!("Wrong bit depth"),
}
// 3rd band (blue)
match decoder.read_chunk(chunks / 3 * 2).unwrap() {
DecodingResult::U8(chunk) => {
assert_eq!(chunk[0], 30);
}
_ => panic!("Wrong bit depth"),
}
}
#[test]
fn test_div_zero() {
use tiff::{TiffError, TiffFormatError};
@@ -301,7 +400,7 @@ fn test_div_zero() {
match err {
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
unexpected => panic!("Unexpected error {}", unexpected),
unexpected => panic!("Unexpected error {unexpected}"),
}
}
@@ -319,7 +418,7 @@ fn test_too_many_value_bytes() {
match error {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
unexpected => panic!("Unexpected error {unexpected}"),
}
}
@@ -333,9 +432,7 @@ fn fuzzer_testcase5() {
178, 178, 178,
];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();
let _ = decoder.read_image().unwrap_err();
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -352,7 +449,7 @@ fn fuzzer_testcase1() {
}
#[test]
fn test_stripped_image_overflow() {
fn fuzzer_testcase6() {
let image = [
73, 73, 42, 0, 8, 0, 0, 0, 8, 0, 0, 1, 4, 0, 1, 0, 0, 0, 100, 0, 0, 148, 1, 1, 4, 0, 1, 0,
0, 0, 158, 0, 0, 251, 3, 1, 3, 255, 254, 255, 255, 0, 1, 0, 0, 0, 6, 1, 3, 0, 1, 0, 0, 0,
@@ -361,14 +458,7 @@ fn test_stripped_image_overflow() {
178, 178,
];
let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap();
let err = decoder.read_image().unwrap_err();
match err {
tiff::TiffError::LimitsExceeded => {}
unexpected => panic!("Unexpected error {}", unexpected),
}
let _ = tiff::decoder::Decoder::new(std::io::Cursor::new(&image)).unwrap_err();
}
#[test]
@@ -459,7 +549,7 @@ fn timeout() {
match error {
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
e => panic!("Unexpected error {:?}", e),
e => panic!("Unexpected error {e:?}"),
}
}
@@ -477,3 +567,108 @@ fn test_predictor_3_rgb_f32() {
fn test_predictor_3_gray_f32() {
test_image_sum_f32("predictor-3-gray-f32.tif", ColorType::Gray(32), 20008.275);
}
#[test]
#[cfg(feature = "zstd")]
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,
);
}
+154 -1
View File
@@ -36,6 +36,47 @@ fn encode_decode() {
let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
@@ -75,6 +116,47 @@ fn encode_decode_big() {
let mut decoder = Decoder::new(&mut file).unwrap();
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
assert_eq!(decoder.dimensions().unwrap(), (100, 100));
let mut all_tags = decoder
.tag_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();
all_tags.sort_by_key(|(t, _)| t.to_u16());
assert_eq!(
all_tags,
vec![
(Tag::ImageWidth, ifd::Value::Unsigned(100)),
(Tag::ImageLength, ifd::Value::Unsigned(100)),
(
Tag::BitsPerSample,
ifd::Value::List(vec![
ifd::Value::Short(8),
ifd::Value::Short(8),
ifd::Value::Short(8)
])
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::UnsignedBig(27)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::UnsignedBig(30000)),
(Tag::XResolution, ifd::Value::Rational(1, 1)),
(Tag::YResolution, ifd::Value::Rational(1, 1)),
(Tag::ResolutionUnit, ifd::Value::Short(1)),
(Tag::Artist, ifd::Value::Ascii("Image-tiff".into())),
(Tag::Predictor, ifd::Value::Short(1)),
(
Tag::SampleFormat,
ifd::Value::List(vec![
ifd::Value::Short(1),
ifd::Value::Short(1),
ifd::Value::Short(1)
])
),
]
);
assert_eq!(
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
@@ -188,7 +270,9 @@ macro_rules! test_roundtrip {
}
test_roundtrip!(test_u8_roundtrip, U8, u8);
test_roundtrip!(test_i8_roundtrip, I8, i8);
test_roundtrip!(test_u16_roundtrip, U16, u16);
test_roundtrip!(test_i16_roundtrip, I16, i16);
test_roundtrip!(test_u32_roundtrip, U32, u32);
test_roundtrip!(test_u64_roundtrip, U64, u64);
test_roundtrip!(test_f32_roundtrip, F32, f32);
@@ -199,6 +283,11 @@ fn test_gray_u8_roundtrip() {
test_u8_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_gray_i8_roundtrip() {
test_i8_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_rgb_u8_roundtrip() {
test_u8_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
@@ -209,11 +298,21 @@ fn test_cmyk_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
}
#[test]
fn test_cmyka_u8_roundtrip() {
test_u8_roundtrip::<colortype::CMYKA8>("cmyk-4c-8b.tiff", ColorType::CMYKA(8));
}
#[test]
fn test_gray_u16_roundtrip() {
test_u16_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_gray_i16_roundtrip() {
test_i16_roundtrip::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_rgb_u16_roundtrip() {
test_u16_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
@@ -522,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);
}
}
+20 -19
View File
@@ -6,7 +6,7 @@ use tiff::{
encoder::{
colortype::{self, ColorType},
compression::*,
TiffEncoder, TiffValue,
Compression, TiffEncoder, TiffValue,
},
};
@@ -18,15 +18,12 @@ trait TestImage<const NUM_CHANNELS: usize>: From<Vec<<Self::Color as ColorType>:
fn reference_data(&self) -> &[<Self::Color as ColorType>::Inner];
fn generate_pixel(x: u32, y: u32) -> [<Self::Color as ColorType>::Inner; NUM_CHANNELS];
fn compress<C: Compression, W: Write + Seek>(
&self,
encoder: &mut TiffEncoder<W>,
compression: C,
) where
fn compress<W: Write + Seek>(&self, encoder: &mut TiffEncoder<W>)
where
[<Self::Color as ColorType>::Inner]: TiffValue,
{
let image = encoder
.new_image_with_compression::<Self::Color, C>(Self::WIDTH, Self::HEIGHT, compression)
.new_image::<Self::Color>(Self::WIDTH, Self::HEIGHT)
.unwrap();
image.write_data(self.reference_data()).unwrap();
}
@@ -94,7 +91,7 @@ impl TestImage<1> for TestImageGrayscale {
}
}
fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
fn encode_decode_with_compression(compression: Compression) {
let mut data = Cursor::new(Vec::new());
let image_rgb = TestImageColor::generate();
@@ -103,9 +100,11 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
// Encode tiff with compression
{
// Create a multipage image with 2 images
let mut encoder = TiffEncoder::new(&mut data).unwrap();
image_rgb.compress(&mut encoder, compression.clone());
image_grayscale.compress(&mut encoder, compression);
let mut encoder = TiffEncoder::new(&mut data)
.unwrap()
.with_compression(compression);
image_rgb.compress(&mut encoder);
image_grayscale.compress(&mut encoder);
}
// Decode tiff
@@ -117,7 +116,7 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
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()
);
@@ -127,7 +126,7 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
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()
);
@@ -136,22 +135,24 @@ fn encode_decode_with_compression<C: Compression + Clone>(compression: C) {
#[test]
fn encode_decode_without_compression() {
encode_decode_with_compression(Uncompressed::default());
encode_decode_with_compression(Compression::Uncompressed);
}
#[test]
#[cfg(feature = "lzw")]
fn encode_decode_with_lzw() {
encode_decode_with_compression(Lzw::default());
encode_decode_with_compression(Compression::Lzw);
}
#[test]
#[cfg(feature = "deflate")]
fn encode_decode_with_deflate() {
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Fast));
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Balanced));
encode_decode_with_compression(Deflate::with_level(DeflateLevel::Best));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Fast));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Balanced));
encode_decode_with_compression(Compression::Deflate(DeflateLevel::Best));
}
#[test]
fn encode_decode_with_packbits() {
encode_decode_with_compression(Packbits::default());
encode_decode_with_compression(Compression::Packbits);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+231
View File
@@ -0,0 +1,231 @@
extern crate tiff;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::encoder::{colortype, Predictor, TiffEncoder};
use tiff::ColorType;
use std::fs::File;
use std::io::{Cursor, Seek, SeekFrom};
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_predict {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
) {
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
let file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(file).expect("Cannot create decoder!");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
let mut predicted = Vec::with_capacity(image_data.len());
C::horizontal_predict(&image_data, &mut predicted);
let sample_size = C::SAMPLE_FORMAT.len();
(0..sample_size).for_each(|i| {
assert_eq!(predicted[i], image_data[i]);
});
(sample_size..image_data.len()).for_each(|i| {
predicted[i] = predicted[i].wrapping_add(predicted[i - sample_size]);
assert_eq!(predicted[i], image_data[i]);
});
}
};
}
test_predict!(test_u8_predict, U8, u8);
test_predict!(test_i8_predict, I8, i8);
test_predict!(test_u16_predict, U16, u16);
test_predict!(test_i16_predict, I16, i16);
test_predict!(test_u32_predict, U32, u32);
test_predict!(test_u64_predict, U64, u64);
#[test]
fn test_gray_u8_predict() {
test_u8_predict::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_gray_i8_predict() {
test_i8_predict::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_rgb_u8_predict() {
test_u8_predict::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
}
#[test]
fn test_cmyk_u8_predict() {
test_u8_predict::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
}
#[test]
fn test_gray_u16_predict() {
test_u16_predict::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_gray_i16_predict() {
test_i16_predict::<colortype::GrayI16>("minisblack-1c-i16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_rgb_u16_predict() {
test_u16_predict::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
}
#[test]
fn test_cmyk_u16_predict() {
test_u16_predict::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
}
#[test]
fn test_gray_u32_predict() {
test_u32_predict::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
}
#[test]
fn test_rgb_u32_predict() {
test_u32_predict::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
}
#[test]
fn test_gray_u64_predict() {
test_u64_predict::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
}
#[test]
fn test_rgb_u64_predict() {
test_u64_predict::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
}
#[test]
fn test_ycbcr_u8_predict() {
test_u8_predict::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
macro_rules! test_predict_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
) {
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");
assert_eq!(decoder.colortype().unwrap(), expected_type);
let image_data = match decoder.read_image().unwrap() {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
let mut file = Cursor::new(Vec::new());
{
let mut tiff = TiffEncoder::new(&mut file)
.unwrap()
.with_predictor(Predictor::Horizontal);
let (width, height) = decoder.dimensions().unwrap();
tiff.write_image::<C>(width, height, &image_data).unwrap();
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) =
decoder.read_image().expect("Decoding image failed")
{
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
}
}
}
};
}
test_predict_roundtrip!(test_u8_predict_roundtrip, U8, u8);
test_predict_roundtrip!(test_i8_predict_roundtrip, I8, i8);
test_predict_roundtrip!(test_u16_predict_roundtrip, U16, u16);
test_predict_roundtrip!(test_i16_predict_roundtrip, I16, i16);
test_predict_roundtrip!(test_u32_predict_roundtrip, U32, u32);
test_predict_roundtrip!(test_u64_predict_roundtrip, U64, u64);
#[test]
fn test_gray_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::Gray8>("minisblack-1c-8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_gray_i8_predict_roundtrip() {
test_i8_predict_roundtrip::<colortype::GrayI8>("minisblack-1c-i8b.tiff", ColorType::Gray(8));
}
#[test]
fn test_rgb_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::RGB8>("rgb-3c-8b.tiff", ColorType::RGB(8));
}
#[test]
fn test_cmyk_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::CMYK8>("cmyk-3c-8b.tiff", ColorType::CMYK(8));
}
#[test]
fn test_gray_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::Gray16>("minisblack-1c-16b.tiff", ColorType::Gray(16));
}
#[test]
fn test_gray_i16_predict_roundtrip() {
test_i16_predict_roundtrip::<colortype::GrayI16>(
"minisblack-1c-i16b.tiff",
ColorType::Gray(16),
);
}
#[test]
fn test_rgb_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::RGB16>("rgb-3c-16b.tiff", ColorType::RGB(16));
}
#[test]
fn test_cmyk_u16_predict_roundtrip() {
test_u16_predict_roundtrip::<colortype::CMYK16>("cmyk-3c-16b.tiff", ColorType::CMYK(16));
}
#[test]
fn test_gray_u32_predict_roundtrip() {
test_u32_predict_roundtrip::<colortype::Gray32>("gradient-1c-32b.tiff", ColorType::Gray(32));
}
#[test]
fn test_rgb_u32_predict_roundtrip() {
test_u32_predict_roundtrip::<colortype::RGB32>("gradient-3c-32b.tiff", ColorType::RGB(32));
}
#[test]
fn test_gray_u64_predict_roundtrip() {
test_u64_predict_roundtrip::<colortype::Gray64>("gradient-1c-64b.tiff", ColorType::Gray(64));
}
#[test]
fn test_rgb_u64_predict_roundtrip() {
test_u64_predict_roundtrip::<colortype::RGB64>("gradient-3c-64b.tiff", ColorType::RGB(64));
}
#[test]
fn test_ycbcr_u8_predict_roundtrip() {
test_u8_predict_roundtrip::<colortype::YCbCr8>("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
}
+41
View File
@@ -0,0 +1,41 @@
use tiff::decoder::Decoder;
// 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());
}
Binary file not shown.