Compare commits

..
Author SHA1 Message Date
A. Molzer 082a35adb9 Deprecate unofficial value types
Seems they were introduced with BigTiff but since the enumeration of the
decoder had no obligation of introducing discriminant values the list
was not cross-referenced to libtiff. Hence, some rational types exist
with no basis in decoding.
2026-01-08 14:25:09 +01:00
Aurelia Molzer 923df5f0bd Merge pull request #321 from image-rs/forward-tiff-value-for-vec-and-array
Add diagnostic helps for TiffValue proxy implementations
2026-01-06 20:05:30 +00:00
A. Molzer c4328644b8 Fixup clippy from MSRV 1.85 bump 2026-01-06 17:30:51 +01:00
A. Molzer 27bfcf5562 Bump MSRV to 1.85
This gives us the diagnostic items. As a side effect we will have
`expect` for lints from 1.81 to apply in later PRs. It's time I think.
2026-01-06 17:27:51 +01:00
A. Molzer 7d498a0284 Add TiffValue proxy implementations
We have a fundamental choice here: We could add more proxy
implementations for `Vec` and `Box` to follow `&'_ T`, for some types
that indirections is clear. Vec and arrays are also both very natural
ways of expressing a repeated value. However, passing an allocated value
to the interface forces us to drop it inside. We might be able to
utilize the allocation for an in-place byteswap in a future increment
but not with the current design.

So, overall, I think that inherently `Drop`-y types should be avoided in
these implementations. Instead, we can use the amazing diagnostic tools
in the compiler to just help the users navigate the type system. We can
still provide the impl for arrays.

For context, see: https://users.rust-lang.org/t/what-type-is-u16/137395

Now they see:

	    |
	 38 |         .write_tag(tiff::tags::Tag::GeoKeyDirectoryTag, &geokey);
	    |                                                          ^^^^^^ the trait `TiffValue` is not implemented for `&Vec<u16>`
	    |
	    = note: the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)
	    = note: the trait is implemented for shared references to values
	    = note: the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)
	    = note: the trait is implemented for arrays if it is implemented for a slice
	    = note: values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`
	    = help: the following other types implement trait `TiffValue`:

Previously the compiler would both strip the `&` in its first message
(due to our forwarding impl) and not list any way of getting a
reference-to-slice.
2026-01-06 17:26:18 +01:00
Aurelia Molzer 5b21bcc75c Merge pull request #320 from image-rs/release-0.11
Release notes for 0.11
2026-01-05 22:53:18 +00:00
A. Molzer fe305ec24a Bump version to 0.11 2026-01-05 22:49:00 +01:00
Aurelia Molzer a3ab252168 Merge pull request #319 from image-rs/ycbcr-subsampling
Recognize Ycbcr/Chroma subsampling tags
2026-01-05 20:57:17 +00:00
A. Molzer bd64e438a4 Add note about subsampling support to decoder 2026-01-05 21:46:59 +01:00
A. Molzer fa225e820b Exempt JPEG compression from upsampling restriction
I didn't realize we support this in a slightly weird manner where the
tag is completely ignored and zune-jpeg performs the upsampling as
indicated in the JPEG stream. This is a recurring problem with the
compression scheme and tags in TIFF. In the end, decoder can only handle
the data as indicated by the JPEG steam and while the specification
requires the tags to be consistent with the underlying data, sometimes
they are not. The useful fallback is of course to ignore the tag.

Ignoring the tag of course buts us in a weird spot for zero-copy
decoding since the buffer indication to the caller will be incorrect.
For now, maybe the best way of treating subsampling is to upsample all
the cases when we add support.
2026-01-05 21:38:32 +01:00
A. Molzer ea373bae00 Fix YCbCr encoding not having subsampling 2026-01-05 21:14:40 +01:00
A. Molzer 23d1867f75 Recognize YCbCr/Chomra subsampling tags
In particular we avoid mistaking the absence of this tag for an absence
of subsampling. The specification instead requires it to mean half the
samples in both directions—a mode that we do not support yet. Instead of
returning erroneous samples or an error in the data stream (which would
appear too short) we return an error when asked to layout the read
buffer for these images.
2026-01-05 21:14:40 +01:00
Aurelia Molzer 234b2ba677 Merge pull request #318 from image-rs/release-0.11
Fix a couple of nits
2026-01-05 17:50:30 +00:00
Dario Bartussek a63a01319c Made Tag::from_u16, from_u16_exhaustive and to_u16 const
(cherry picked from commit 5c3c25ec2d152a672b0b1307efc69ccfcb0bea29)
2026-01-05 18:34:36 +01:00
A. Molzer efd2a6dbd8 Nit: documentation links to wrong item 2026-01-05 18:31:57 +01:00
Aurelia Molzer d38fba4268 Merge pull request #317 from image-rs/cielab
Add ICC Lab interpretation
2025-12-30 05:55:33 +00:00
A. Molzer 1732481b01 Add ICC Lab interpretation
The CIE Lab interpretation uses heterogeneous samples, and the ITU Lab
encoding requires the Tag Decode (433, 0x01B1) which has default values
depending on bit depth and convering values into floating points, probably.

See also Photometric Interpretation: <https://web.archive.org/web/20050309053132/http://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html>
See also Tag Decode: <https://web.archive.org/web/20050309072856/http://www.awaresystems.be/imaging/tiff/tifftags/decode.html>
2025-12-30 06:51:20 +01:00
Aurelia Molzer 355ecc8ad9 Merge pull request #313 from image-rs/decode-planar-data
Implement support for planar data
2025-12-29 19:05:39 +00:00
A. Molzer 2d90179602 Soften deprecation of Decoder::read_image 2025-12-29 19:49:09 +01:00
A. Molzer 5a3b11314e Rewrite _to_buffer variants
We previously handed it a wrapper that was supposedly type-safe but
really internally cast the buffer to bytes. That was rather pointless as
the `_bytes` method could always be used instead. We replace the
semantics by a utility which allocates an appropriate buffer with the
internal limits, wrapping the exact sequence identified previously in
the example as a pattern.
2025-12-28 22:23:59 +01:00
A. Molzer 5150e91bbb Document and deprecate read_image 2025-12-28 22:02:43 +01:00
A. Molzer 6d56d014a1 Sample types in layouts, resize DecoderResult
By adding the type interpretation to `BufferLayoutPreference` (simply
computed from the same fields that are trivial to fetch in
`ReadoutLayout` with the rest) we can allocate an appropriate
`DecoderResult` without any `Decoder` instance. We still want a Limits
instance though. This lets a caller read _typed_ data from the `_bytes`
methods which perform less allocations. It also lets them read multiple
planes in a more convenient interface. See, `read_image` will keep its
planar treatment bug for a little bit so you don't accidentally break
during an upgrade.

This also implies less code is being monomorphized, just a bonus.
2025-12-28 21:06:59 +01:00
A. Molzer af8a8d660f Write decoded planar data in example 2025-12-28 18:48:41 +01:00
A. Molzer b1c74495ce Allow extraneous extra samples beyond alpha 2025-12-28 15:09:42 +01:00
A. Molzer 1f02ff8ca1 Add method to retrieve planes of a tile
This is well-defined even if we add sub-sampling support later on. We
can adjust the layout type accordingly with factors per each plane. Note
that the layout, in theory, does not depend on the index of the unit if
we describe all planes for a multi-planar layout. The specification
requires that the subsampling factors divide the tile sizes. However we
have to handle non-planar images here and this leaves us open to other
potential parameters.
2025-12-28 15:06:10 +01:00
A. Molzer 5b9dc30919 Remove badly defined layout method 2025-12-10 21:59:03 +01:00
A. Molzer c3882b252c Cleanup documentation 2025-11-27 23:56:57 +01:00
A. Molzer 6fe21da979 Simplify layout proxy methods 2025-11-27 23:56:57 +01:00
A. Molzer 0de1261948 Simplify responsibility of testing buffer size 2025-11-27 23:56:56 +01:00
A. Molzer 6439880e3d Simplify layout structs 2025-11-27 23:56:13 +01:00
A. Molzer dce1c40bd5 Move layout responsibility into ReadoutLayout 2025-11-27 23:56:08 +01:00
A. Molzer 5d3a5242ea Add plane selection to allocating result_extent 2025-11-26 19:18:27 +01:00
A. Molzer 58543962f3 Add example that exercises decoding (no output) 2025-11-23 19:54:21 +01:00
A. Molzer 95f2d475fa Consolidate layout information type
We currently have one type in decoder/image.rs and another in
decoder/mod.rs which each contributes part of the computation for laying
out the output buffer. Confusingly though they rely on shared
implementation details such as determining the number of channels that
are written and their bit depth. We should have one shared method and
resulting struct instead.
2025-11-23 19:51:26 +01:00
A. Molzer f9eaf9632a Expand PreferredLayout for planar information 2025-11-17 21:11:28 +01:00
A. Molzer 61b3027f38 Test reading planes of a whole image 2025-11-13 23:38:15 +01:00
A. Molzer 65662c4834 Expand requested plane when reading chunk 2025-11-12 23:42:49 +01:00
Aurelia Molzer 985f3822c8 Merge pull request #311 from image-rs/decode-and-encode-extra-samples
Decode and encode extra samples
2025-11-12 10:49:06 +00:00
A. Molzer 2eef9ac088 Encoding of extra samples, roundtrip test 2025-11-12 01:41:38 +01:00
Aurelia Molzer f6d95249dd Merge pull request #310 from image-rs/directory-improvements
Add some utilities around Directory
2025-11-12 00:24:57 +00:00
A. Molzer 8517bb81db Decode with ExtraSamples
These samples are normal bits in the encoded (e.g. compressed) stream of
each tile or row of the image. However, when considering the photometric
interpretation of an image these samples should be discarded. They are
only relevant for multiband.

For the most part we previously rejected these images due to a mismatch
between sample count and the color as indicated by the interpretation.
Only, for alpha we never really verified if the extra channel is indeed
alpha or something unrelated. We don't have a perfect solution as
GeoTiff motivates us having a MultiBand pseudo-color output
configuration that has no related photometric interpretation and borrows
the usual gray tone instead. We can only really distinguish this from a
gray-alpha image through looking at the values of the ExtraSamples
vector. Then again we do not yet support GrayAlpha anyways.

As a simplification for the discarding process we only support full-byte
extra samples and require all samples to be the same bit-depth, not only
those which we return. This is blessed by Tiff's recommendations for
baseline decoding as well as libtiff.
2025-11-12 01:23:12 +01:00
19 changed files with 1451 additions and 358 deletions

No files matched your search

+4 -4
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.74.0", stable, beta, nightly]
rust: ["1.85.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.85.0' }}
- name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.74.0' }}
if: ${{ matrix.rust == '1.85.0' }}
run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build
run: cargo build -v
- name: test
if: ${{ matrix.rust != '1.74.0' }}
if: ${{ matrix.rust != '1.85.0' }}
run: cargo test -v && cargo doc -v
features:
+49 -1
View File
@@ -1,7 +1,55 @@
# Version 0.10.4 (unreleased)
# Version 0.11.0
- `Directory` now implements `FromIterator<(Tag, Value)>`.
Changes:
- The decoder now interprets the `ExtraSamples` tag. The sample count must now
more strict match the expected value with alpha channels only allowing for
explicitly denoted unassociated or associated alpha. This effects the
indicated color type when decoding images with additional samples indicated
as unspecified relation. Previously, these may have been interpreted as
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
interpretation of RGB).
- The decoder handles planar data, current limited to non-subsampled channels.
The `Decoder::read_image` method return planes one-after-another depending on
the size of the buffer that was passed.
- `Decoder::read_image_to_buffer` now takes `&mut DecodingResult` and resizes
it according to the required layout. Previously, a borrowed `DecodingBuffer`
was passed which can be replaced by calling `as_bytes_mut` and
`read_image_bytes`.
- Several methods of tags are now `const`. Note that does not guarantee any
particular value when calling these methods.
Fixes:
- Fix a bug in the uncompressed encoder that could lead to short writes, i.e.
data silently dropped when the underlying writer did not accept all data in a
single write call.
- Encoding YCbCr data now writes the `ChromaSubsampling` tag as `(1, 1)` to
indicate no subsampling, instead of leaving it at its default of `(2, 2)`.
- The decoder will reject subsampled YCbCr data as there is no upsampling
routine, except for JPEG compressed images where the JPEG decoders handles
this. Since the buffer in that case indicates a full-sized plane for all
color samples any future support for the tag will upsample all planes (at
least within this major version).
Additions:
- Added support for the `CieLab` color type.
- Added `DecodingResult::resize_to` to create a buffer with a matching sample
type and dimensions.
- Added `ByteOrder::native` to access the platform's native endianness.
- Added `ByteOrder::convert` to change the byte-order of values in a byte
buffer, depending on their `Type` as described at runtime.
- Added `DirectoryOffset::new` to encode a directory whose offset is known to
the caller but that has not been written through the encoder itself.
- Added `Encoder::extra_samples` to encode images with more samples than their
color's trait implementation would otherwise suggest.
- Added `DirectoryEncoder::extend_from` to encode multiple tag entries from a
directory whose values were written to the file by means outside the
encoder's control.
- Added `Decoder::read_coding_unit_bytes` to retrieve data of corresponding
coordinates from potentially planar data, which is encoded in multiple chunks
of the file.
# Version 0.10.3
New features:
+2 -2
View File
@@ -1,11 +1,11 @@
[package]
name = "tiff"
version = "0.10.3"
version = "0.11.0"
edition = "2021"
resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.74.0"
rust-version = "1.85.0"
license = "MIT"
description = "TIFF decoding and encoding library in pure Rust"
+103
View File
@@ -0,0 +1,103 @@
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(image) = std::env::args_os().nth(1) else {
eprintln!("Usage: decode FILE");
return Ok(());
};
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::new(io)?;
let mut data = DecodingResult::I8(vec![]);
for i in 0u32.. {
let colortype = reader.colortype()?;
let dimensions = reader.dimensions()?;
let layout = reader.read_image_to_buffer(&mut data)?;
debug_planes(i, &mut data, &layout, &colortype, dimensions);
if !reader.more_images() {
break;
}
reader.next_image()?
}
Ok(())
}
fn debug_planes(
index: u32,
data: &mut DecodingResult,
layout: &BufferLayoutPreference,
colortype: &tiff::ColorType,
(width, height): (u32, u32),
) {
let (depth, mut tupltype) = match colortype {
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
tiff::ColorType::RGB(_) => (3, "RGB"),
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
tiff::ColorType::Palette(_) => (1, "PALETTE"),
tiff::ColorType::CMYK(_) => (4, "CMYK"),
tiff::ColorType::Multiband { num_samples: 2, .. } => (2, "GRAYSCALE_ALPHA"),
_ => {
eprintln!("Unsupported color type for PAM output: {:?}", colortype);
return;
}
};
let maxval = match data.as_buffer(0) {
DecodingBuffer::U8(_) => u64::from(u8::MAX),
DecodingBuffer::U16(_) => u64::from(u16::MAX),
DecodingBuffer::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX,
// Eh, this is a good effort I guess.
DecodingBuffer::I8(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i8::MAX as u8)
}
DecodingBuffer::I16(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i16::MAX as u16)
}
DecodingBuffer::I32(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
u64::from(i32::MAX as u32)
}
DecodingBuffer::I64(items) => {
items.iter_mut().for_each(|v| *v = 0.max(*v));
i64::MAX as u64
}
// Not supported by PAM.
DecodingBuffer::F16(_) | DecodingBuffer::F32(_) | DecodingBuffer::F64(_) => return,
};
let byte_len = if layout.planes > 1 {
tupltype = "GRAYSCALE";
layout.plane_stride.unwrap().get()
} else {
data.as_buffer(0).byte_len()
};
// Write out this image's planes to PAM files.
let header = format!("P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {depth}\nMAXVAL {maxval}\nTUPLTYPE {tupltype}\nENDHDR\n");
for (plane, plane_data) in data
.as_buffer(0)
.as_bytes_mut()
.chunks_exact(byte_len)
.enumerate()
{
let filename = format!("image_{index:03}_plane_{plane:03}.pam");
let mut file = std::fs::File::create(filename).expect("Failed to create output file");
use std::io::Write;
file.write_all(header.as_bytes())
.expect("Failed to write PAM header");
file.write_all(plane_data)
.expect("Failed to write PAM data");
}
}
+22 -8
View File
@@ -10,8 +10,8 @@ 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, SignedByte, SignedShort, Unsigned, UnsignedBig,
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
SignedByte, SignedShort, Unsigned, UnsignedBig,
};
#[allow(unused_qualifications)]
@@ -30,8 +30,16 @@ pub enum Value {
Double(f64),
List(Vec<Value>),
Rational(u32, u32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
RationalBig(u64, u64),
SRational(i32, i32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
SRationalBig(i64, i64),
Ascii(String),
Ifd(u32),
@@ -170,7 +178,8 @@ impl Value {
Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
RationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => {
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
}
Ifd(val) => Ok(vec![val]),
@@ -220,7 +229,8 @@ impl Value {
new_vec.push(numerator);
new_vec.push(denominator);
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(i32::try_from(numerator)?);
new_vec.push(i32::try_from(denominator)?);
}
@@ -234,7 +244,8 @@ impl Value {
Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
}
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
@@ -283,7 +294,8 @@ impl Value {
Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
#[expect(deprecated)]
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
@@ -301,7 +313,8 @@ impl Value {
new_vec.push(numerator.into());
new_vec.push(denominator.into());
}
SRationalBig(numerator, denominator) => {
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(numerator);
new_vec.push(denominator);
}
@@ -315,7 +328,8 @@ impl Value {
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]),
#[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
}
}
+484 -62
View File
@@ -4,14 +4,14 @@ use super::tag_reader::TagReader;
use super::ChunkType;
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
CompressionMethod, ExtraSamples, 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)]
@@ -69,6 +69,11 @@ pub(crate) struct Image {
pub height: u32,
pub bits_per_sample: u8,
pub samples: u16,
/// The `ExtraSamples`, defaulting to empty if not given.
pub extra_samples: Vec<ExtraSamples>,
/// Number of samples that belong to the photometric interpretation, samples except
/// `ExtraSamples` (338, 0x0152) tag.
pub photometric_samples: u16,
pub sample_format: SampleFormat,
pub photometric_interpretation: PhotometricInterpretation,
pub compression_method: CompressionMethod,
@@ -80,6 +85,58 @@ pub(crate) struct Image {
pub tile_attributes: Option<TileAttributes>,
pub chunk_offsets: Vec<u64>,
pub chunk_bytes: Vec<u64>,
pub chroma_subsampling: (u16, u16),
}
/// Describes how to read a tile-aligned portion of the image.
#[derive(Clone)]
pub(crate) struct ReadoutLayout {
/// The planar configuration, which applies to both the underlying image and the output buffer.
/// This may be relaxed if we find a clean enough way to provide it.
pub planar_config: PlanarConfiguration,
/// The sample interpretation (interpret with planar_config).
///
/// FIXME: we should not require this here. The ability to turn out the raw bytes from the
/// sample arrays is very different from turning out interpretable color. Firstly we can always
/// readout `Multiband` but currently only use that ColorType in special circumstances (it must
/// not overlap cases where actually want to use a ColorType).
///
/// And then we have CIE Lab, which uses a tuple of `(u8, i8, i8)`, that is still filterable
/// but still not represented by any of our `DecoderResult` variants. Other color variants
/// depend on extra tags (YCbCrCoefficients/0x0211) and we don't have a good side channel to
/// tag the output with all that TIFF specific information, so arguably we should process and
/// apply those to the data so it becomes a self-contained representation.
///
/// This should be computed at a higher level, in `Decoder`, instead.
pub color: ColorType,
/// The number of bytes from one row to another.
pub minimum_row_stride: usize,
/// The format of samples (assumed uniform for now, same with depth of `ColorType`).
pub sample_format: SampleFormat,
/// Number of bytes to advance in output per row.
pub row_stride: usize,
/// Number of bytes to advance in output per chunk in width.
pub chunk_row_stride: usize,
/// Number of bytes to advance in output per chunk in height.
pub chunk_col_stride: usize,
/// Number of bytes in output from one plane to another.
pub plane_stride: usize,
/// Bits per sample in the encoded data.
pub tiff_bits_per_sample: u8,
/// Number of samples in the encoded data.
pub tiff_samples: u16,
/// Dimensions of the underlying rectangular chunks (tile or strips).
pub tiff_chunk_dimensions: (u32, u32),
/// Number of bytes in the underlying data with all samples per row of chunks.
pub tiff_row_bytes: usize,
/// Chunks until wrapping to the next row of chunks.
pub chunks_across: u32,
/// Chunks to advance to get to the next plane of chunks.
pub chunks_per_plane: u32,
}
impl Image {
@@ -134,10 +191,30 @@ impl Image {
.map(Value::into_u16)
.transpose()?
.unwrap_or(1);
if samples == 0 {
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
}
let extra_samples = match tag_reader.find_tag(Tag::ExtraSamples)? {
Some(n) => n.into_u16_vec()?,
None => vec![],
};
let extra_samples = extra_samples
.into_iter()
.map(|x| ExtraSamples::from_u16(x).unwrap_or(ExtraSamples::Unspecified))
.collect::<Vec<_>>();
let photometric_samples = match usize::from(samples).checked_sub(extra_samples.len()) {
None => {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
Some(n) => n as u16,
};
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
Some(vals) => {
let sample_format: Vec<_> = vals
@@ -164,7 +241,7 @@ impl Image {
// 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 {
if bits_per_sample.len() != usize::from(samples) && bits_per_sample.len() != 1 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
@@ -199,6 +276,25 @@ impl Image {
.transpose()?
.unwrap_or(PlanarConfiguration::Chunky);
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
let [a, b] = subsamples.as_slice() else {
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
Tag::ChromaSubsampling,
subsamples.len(),
)));
};
// ImageWidth and ImageLength are constrained to be integer multiples of
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
// have the same constraints. RowsPerStrip must be an integer multiple of
// YCbCrSubsampleVert.
(*a, *b)
} else {
(2, 2)
};
let planes = match planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => samples,
@@ -297,6 +393,8 @@ impl Image {
height,
bits_per_sample: bits_per_sample[0],
samples,
extra_samples,
photometric_samples,
sample_format,
photometric_interpretation,
compression_method,
@@ -308,19 +406,24 @@ impl Image {
tile_attributes,
chunk_offsets,
chunk_bytes,
chroma_subsampling,
})
}
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
let is_alpha_extra_samples = matches!(
self.extra_samples.as_slice(),
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
);
match self.photometric_interpretation {
PhotometricInterpretation::RGB => match self.samples {
3 => Ok(ColorType::RGB(self.bits_per_sample)),
PhotometricInterpretation::RGB => match self.photometric_samples {
3 => Ok(if is_alpha_extra_samples {
ColorType::RGBA(self.bits_per_sample)
} else {
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
// values of the SamplesPerPixel and BitsPerSample fields.
// > -- TIFF 6.0 Specification, Section 7, Additional Baseline requirements.
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
@@ -328,8 +431,12 @@ impl Image {
),
)),
},
PhotometricInterpretation::CMYK => match self.samples {
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
PhotometricInterpretation::CMYK => match self.photometric_samples {
4 => Ok(if is_alpha_extra_samples {
ColorType::CMYKA(self.bits_per_sample)
} else {
ColorType::CMYK(self.bits_per_sample)
}),
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
@@ -338,7 +445,7 @@ impl Image {
),
)),
},
PhotometricInterpretation::YCbCr => match self.samples {
PhotometricInterpretation::YCbCr => match self.photometric_samples {
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
@@ -351,6 +458,9 @@ impl Image {
// later called when that interpretation is read. That function does not support
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
// Note: compatibility with previous implementation requires us to return extra
// samples as `Multiband`. For gray images however the better choice would be
// returning a `Gray` color, i.e. matching on `photometric_samples` instead.
match self.samples {
1 => Ok(ColorType::Gray(self.bits_per_sample)),
_ => Ok(ColorType::Multiband {
@@ -359,34 +469,63 @@ impl Image {
}),
}
}
// TODO: this is bad we should not fail at this point
PhotometricInterpretation::RGBPalette
| PhotometricInterpretation::TransparencyMask
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
// ```
// struct IccLab /* Interpretation 9* {
// pub L: u8, // SampleFormat::Uint
// pub a: u8, // SampleFormat::Uint, defined as TiffLab::a + 128
// pub b: u8, // SampleFormat::Uint, defined as TiffLab::b + 128
// }
// ```
PhotometricInterpretation::IccLab => match self.photometric_samples {
3 if matches!(self.sample_format, SampleFormat::Uint) => {
Ok(ColorType::Lab(self.bits_per_sample))
}
_ => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
)),
},
// Unsupported due to inherently heterogeneous sample types. This is represented as:
// ```
// struct TiffLab /* Interpretation 8* {
// pub L: u8, // SampleFormat::Uint
// pub a: i8, // SampleFormat::Int
// pub b: i8, // SampleFormat::Int
// }
// ```
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
// Unsupported due to extra unfiltering and conversion steps. We need to find the
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
//
// L* = Decode[0] + Lsample x (Decode[1] - Decode[0]) / (2^n -1)
// …
//
// So we'll have a larger depth in the output and either worry about reducing fractions
// or turn everything into floats. That's a lot of decisions.
PhotometricInterpretation::ItuLab => Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
PhotometricInterpretation::CIELab,
vec![self.bits_per_sample; self.samples as usize],
),
)),
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
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,
compression_method: CompressionMethod,
@@ -483,18 +622,25 @@ impl Image {
/// 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 {
pub(crate) fn samples_per_pixel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => self.samples.into(),
PlanarConfiguration::Chunky => self.samples,
PlanarConfiguration::Planar => 1,
}
}
pub(crate) fn samples_per_out_texel(&self, color: ColorType) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => color.num_samples(),
PlanarConfiguration::Planar => 1,
}
}
/// Number of strips per pixel.
pub(crate) fn strips_per_pixel(&self) -> usize {
pub(crate) fn strips_per_pixel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => 1,
PlanarConfiguration::Planar => self.samples.into(),
PlanarConfiguration::Planar => self.samples,
}
}
@@ -532,14 +678,107 @@ impl Image {
}
}
pub(crate) fn readout_for_image(&self) -> TiffResult<ReadoutLayout> {
let Image { width, height, .. } = *self;
self.readout_for_size(width, height)
}
/// Get the layout for reading out a tile-aligned portion of the image.
///
/// The provided width and height should be less than or equal to the image dimensions.
pub(crate) fn readout_for_size(&self, width: u32, height: u32) -> TiffResult<ReadoutLayout> {
let color = self.colortype()?;
let tiff_samples = self.samples_per_pixel();
let tiff_bits_per_sample = self.bits_per_sample;
let data_samples = self.samples_per_out_texel(color);
let tiff_chunk_dimensions = self.chunk_dimensions()?;
let strips_per_pixel = self.strips_per_pixel();
let data_dimensions = (width, height);
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(tiff_samples))
.ok_or(TiffError::LimitsExceeded)?;
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
let chunk_col_stride: usize = data_row_bits
.div_ceil(8)
.checked_mul(u64::from(tiff_chunk_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let plane_stride: usize = data_row_bits
.div_ceil(8)
.checked_mul(u64::from(data_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let minimum_row_stride = data_row_bytes;
let chunks_across: u32 = data_dimensions.0.div_ceil(tiff_chunk_dimensions.0);
let chunks_per_plane = (self.chunk_offsets.len() as u32) / u32::from(strips_per_pixel);
// We would not get an offset in byte units, sorry, no bit interleaving in the output.
if chunks_across > 1 && chunk_row_bits % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::MisalignedTileBoundaries,
));
}
// Only this color type interprets the tag, which is defined with a default of (2, 2)
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
// The JPEG library does upsampling for us and defines its buffers correctly
// (presumably). All other compression schemes are not supported..
//
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
// off supporting this tag by consistently upsampling, not by adjusting the buffer
// size. At least as a default this makes more sense and is much more permissive in
// case the compression stream disagrees with the tags (we would not have enough / or
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::ChromaSubsampling,
));
}
}
Ok(ReadoutLayout {
planar_config: self.planar_config,
color,
minimum_row_stride,
sample_format: self.sample_format,
row_stride: data_row_bytes,
chunk_row_stride: chunk_row_bytes,
chunk_col_stride,
plane_stride,
tiff_bits_per_sample,
tiff_samples,
tiff_chunk_dimensions,
tiff_row_bytes,
chunks_across,
chunks_per_plane,
})
}
pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> {
let dims = self.chunk_dimensions()?;
match self.chunk_type {
ChunkType::Strip => {
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 rows_per_strip = dims.1;
let strips_per_band = self.height.div_ceil(rows_per_strip);
let strip_height_without_padding = (chunk_index % strips_per_band)
.checked_mul(dims.1)
.and_then(|x| self.height.checked_sub(x))
@@ -568,7 +807,7 @@ impl Image {
&self,
reader: &mut ValueReader<impl Read>,
buf: &mut [u8],
output_row_stride: usize,
layout: &ReadoutLayout,
chunk_index: u32,
) -> TiffResult<()> {
let ValueReader {
@@ -578,10 +817,10 @@ impl Image {
} = reader;
let byte_order = reader.byte_order;
let reader = reader.inner();
// Validate that the color type is supported.
let color_type = self.colortype()?;
let color_type = layout.color;
match color_type {
ColorType::RGB(n)
| ColorType::RGBA(n)
@@ -643,6 +882,7 @@ impl Image {
.ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
return Err(TiffError::LimitsExceeded);
}
@@ -650,34 +890,39 @@ impl Image {
let compression_method = self.compression_method;
let photometric_interpretation = self.photometric_interpretation;
let predictor = self.predictor;
let samples = self.samples_per_pixel();
let samples = layout.tiff_samples;
let data_samples = layout.samples_per_out_texel();
// We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this
// takes up in the output. Each has an associated count of bits per pixel. The first
// dimension, i.e. a ''row'', is the number of pixels that are encoded with bit packing
// while the second is the byte-padded array of each so encoded slices.
//
// During decoding we map the relevant bits from one to the other.
let chunk_dims = self.chunk_dimensions()?;
let data_dims = self.chunk_data_dimensions(chunk_index)?;
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()?;
let chunk_row_bytes: usize = layout.tiff_row_bytes;
let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?;
// 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);
assert!(layout.minimum_row_stride >= data_row_bytes);
assert!(buf.len() >= layout.row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
let is_all_bits = samples == data_samples;
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
let mut reader = Self::create_reader(
reader,
reader.inner(),
compression_method,
*compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims,
)?;
if output_row_stride == chunk_row_bytes {
if is_output_chunk_rows && is_all_bits {
// Here we can read directly into the output buffer itself.
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
reader.read_exact(tile)?;
@@ -690,6 +935,7 @@ impl Image {
predictor,
);
}
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(tile, color_type, self.sample_format)?;
}
@@ -697,7 +943,7 @@ impl Image {
// 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_row_bytes];
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut encoded)?;
let row = &mut row[..data_row_bytes];
@@ -711,11 +957,16 @@ impl Image {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else {
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
} else if is_all_bits {
// We read row-by-row but each row fits in its output buffer.
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
let row = &mut row[..data_row_bytes];
reader.read_exact(row)?;
let used = data_row_bytes.min(chunk_row_bytes);
// Two ways how we get here: we have more bytes in our chunk data than in the image
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
// row stride than the chunk contains data, then we need to fill only the front.
reader.read_exact(&mut row[..used])?;
// Skip horizontal padding
if chunk_row_bytes > data_row_bytes {
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
@@ -729,6 +980,48 @@ impl Image {
byte_order,
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
}
} else {
// The encoded data potentially takes up more space than the output data so we must be
// prepared to discard some of it. That decision is bit-by-bit.
let bits_per_pixel = u32::from(self.bits_per_sample) * u32::from(self.samples);
// Assumes the photometric samples are always the start.. This is slightly problematic.
// To expand spport we should instead have different methods of transforming the read
// buffer data, not only the `compact_photometric_bytes` method below and then choose
// from the right one with supplied parameters. Then we can also bit-for-bit copy with
// a selection for better performance.
let photometric_bit_end = u32::from(self.bits_per_sample) * data_samples as u32;
debug_assert!(bits_per_pixel >= photometric_bit_end);
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::InterpretationWithBits(
self.photometric_interpretation,
vec![self.bits_per_sample; self.samples as usize],
),
));
}
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
let mut encoded = vec![0u8; chunk_row_bytes];
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
reader.read_exact(&mut encoded)?;
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
super::fix_endianness_and_predict(
row,
color_type.bit_depth(),
samples,
byte_order,
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format)?;
}
@@ -737,4 +1030,133 @@ impl Image {
Ok(())
}
/// Turn a contiguous buffer of a whole number of raw sample arrays into a whole number of
/// photometric sample arrays by removing the extra samples in-between.
fn compact_photometric_bytes(
raw: &mut [u8],
row: &mut [u8],
photo_range: &std::ops::Range<u32>,
) {
raw.chunks_exact_mut(photo_range.end as usize)
.zip(row.chunks_exact_mut(photo_range.start as usize))
.for_each(|(src, dst)| {
dst.copy_from_slice(&src[..photo_range.start as usize]);
});
}
}
impl ReadoutLayout {
pub(crate) fn samples_per_out_texel(&self) -> u16 {
match self.planar_config {
PlanarConfiguration::Chunky => self.color.num_samples(),
PlanarConfiguration::Planar => 1,
}
}
// For a concrete chunk, which may be a partial border chunk, the byte length of one row of its
// pixel data.
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
let data_samples = self.samples_per_out_texel();
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample))
.checked_mul(u64::from(data_samples))
.ok_or(TiffError::LimitsExceeded)?;
Ok(data_row_bits.div_ceil(8).try_into()?)
}
pub(crate) fn set_row_stride(&mut self, row_stride: usize) -> Result<(), TiffError> {
if row_stride < self.minimum_row_stride {
return Err(TiffError::UsageError(
UsageError::InsufficientOutputRowStride {
needed: self.minimum_row_stride,
requested: row_stride,
},
));
}
let data_row_bytes = u64::try_from(row_stride)?;
let chunk_col_stride = data_row_bytes
.checked_mul(u64::from(self.tiff_chunk_dimensions.1))
.ok_or(TiffError::LimitsExceeded)?
.try_into()?;
let height = self.plane_stride.checked_div(self.row_stride);
let plane_stride = height
.and_then(|h| data_row_bytes.checked_mul(h as u64))
// If height was zero, or the previous stride was zero, there are no bytes in a plane
.unwrap_or(0)
.try_into()?;
self.row_stride = row_stride;
self.chunk_col_stride = chunk_col_stride;
self.plane_stride = plane_stride;
Ok(())
}
/// Reduce this down to the layout of the output planes.
pub(crate) fn to_plane_layout(&self) -> Result<PlaneLayout, TiffError> {
let num_planes = self.color.num_samples() / self.samples_per_out_texel();
// Using the standard range iterator as checked_add on steroids.
//
// Note: for supporting subsampling, adjust as required.
let mut offset = (0..=usize::MAX).step_by(self.plane_stride);
let plane_offsets = offset.by_ref().take(usize::from(num_planes)).collect();
// Get the past-the-end of the last plane.
//
// This also verifies the `take` above was not short.
let Some(total_bytes) = offset.next() else {
return Err(TiffError::LimitsExceeded);
};
Ok(PlaneLayout {
plane_offsets,
total_bytes,
readout: self.clone(),
})
}
}
/// A `ReadoutLayout` with pre-calculated plane information.
pub(crate) struct PlaneLayout {
/// The underlying readout layout.
pub readout: ReadoutLayout,
/// Buffer offset from one plane of output to the next.
pub plane_offsets: Vec<usize>,
/// Total number of bytes for all planes in given order.
pub total_bytes: usize,
}
impl PlaneLayout {
/// Return the number of planes to extract into the provided buffer.
pub(crate) fn used_planes(&self, buffer: &[impl Sized]) -> TiffResult<u16> {
self.readout.assert_min_layout(buffer)?;
let buffer_len = core::mem::size_of_val(buffer);
// Note: with differently sized planes this is dependent on the plane.
let last_plane_start = buffer_len.checked_sub(self.readout.plane_stride);
// Find how many planes fit into the output buffer.
let used_plane_offsets = self
.plane_offsets
.iter()
.enumerate()
// Find the first plane that would not fit completely at its offset.
.skip_while(|(_, &offset)| last_plane_start >= Some(offset))
.nth(0)
// If all planes fit, use all of them.
.map_or(self.plane_offsets.len(), |(idx, _)| idx);
debug_assert!(
used_plane_offsets <= usize::from(u16::MAX),
"Planes limited by number of samples, which is encoded as u16"
);
Ok(used_plane_offsets as u16)
}
}
+479 -262
View File
@@ -22,6 +22,10 @@ mod image;
mod stream;
mod tag_reader;
/// An index referring to a (rectangular) region of an image.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TiffCodingUnit(pub u32);
/// Result of a decoding process
#[derive(Debug)]
pub enum DecodingResult {
@@ -50,94 +54,87 @@ pub enum DecodingResult {
}
impl DecodingResult {
fn new_u8(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size {
/// Reallocate the buffer to decode all planes of the indicated layout.
pub fn resize_to(
&mut self,
buffer: &BufferLayoutPreference,
limits: &Limits,
) -> Result<(), TiffError> {
let sample_type = buffer.sample_type.ok_or(TiffError::UnsupportedError(
TiffUnsupportedError::UnknownInterpretation,
))?;
let extent = sample_type.extent_for_bytes(buffer.complete_len);
self.resize_to_extent(extent, limits)
}
fn resize_to_extent(
&mut self,
extent: DecodingExtent,
limits: &Limits,
) -> Result<(), TiffError> {
// FIXME: we *can* reuse the allocation sometimes.
*self = extent.to_result_buffer(limits)?;
Ok(())
}
fn new<T: Default + Copy>(
size: usize,
limits: &Limits,
from_fn: fn(Vec<T>) -> Self,
) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / core::mem::size_of::<T>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::U8(vec![0; size]))
Ok(from_fn(vec![T::default(); size]))
}
}
fn new_u8(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
Self::new(size, limits, DecodingResult::U8)
}
fn new_u16(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 2 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::U16(vec![0; size]))
}
Self::new(size, limits, DecodingResult::U16)
}
fn new_u32(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 4 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::U32(vec![0; size]))
}
Self::new(size, limits, DecodingResult::U32)
}
fn new_u64(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 8 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::U64(vec![0; size]))
}
Self::new(size, limits, DecodingResult::U64)
}
fn new_f32(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / std::mem::size_of::<f32>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::F32(vec![0.0; size]))
}
Self::new(size, limits, DecodingResult::F32)
}
fn new_f64(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / std::mem::size_of::<f64>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::F64(vec![0.0; size]))
}
Self::new(size, limits, DecodingResult::F64)
}
fn new_f16(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / std::mem::size_of::<u16>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::F16(vec![f16::ZERO; size]))
}
Self::new(size, limits, DecodingResult::F16)
}
fn new_i8(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / std::mem::size_of::<i8>() {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::I8(vec![0; size]))
}
Self::new(size, limits, DecodingResult::I8)
}
fn new_i16(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 2 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::I16(vec![0; size]))
}
Self::new(size, limits, DecodingResult::I16)
}
fn new_i32(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 4 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::I32(vec![0; size]))
}
Self::new(size, limits, DecodingResult::I32)
}
fn new_i64(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
if size > limits.decoding_buffer_size / 8 {
Err(TiffError::LimitsExceeded)
} else {
Ok(DecodingResult::I64(vec![0; size]))
}
Self::new(size, limits, DecodingResult::I64)
}
/// Get a view of this buffer starting from the nth _sample_ of the current type.
pub fn as_buffer(&mut self, start: usize) -> DecodingBuffer<'_> {
match *self {
DecodingResult::U8(ref mut buf) => DecodingBuffer::U8(&mut buf[start..]),
@@ -182,9 +179,25 @@ pub enum DecodingBuffer<'a> {
}
impl<'a> DecodingBuffer<'a> {
fn as_bytes_mut(&mut self) -> &mut [u8] {
pub fn as_bytes(&self) -> &[u8] {
match self {
DecodingBuffer::U8(ref mut buf) => buf,
DecodingBuffer::U8(buf) => buf,
DecodingBuffer::I8(buf) => bytecast::i8_as_ne_bytes(buf),
DecodingBuffer::U16(buf) => bytecast::u16_as_ne_bytes(buf),
DecodingBuffer::I16(buf) => bytecast::i16_as_ne_bytes(buf),
DecodingBuffer::U32(buf) => bytecast::u32_as_ne_bytes(buf),
DecodingBuffer::I32(buf) => bytecast::i32_as_ne_bytes(buf),
DecodingBuffer::U64(buf) => bytecast::u64_as_ne_bytes(buf),
DecodingBuffer::I64(buf) => bytecast::i64_as_ne_bytes(buf),
DecodingBuffer::F16(buf) => bytecast::f16_as_ne_bytes(buf),
DecodingBuffer::F32(buf) => bytecast::f32_as_ne_bytes(buf),
DecodingBuffer::F64(buf) => bytecast::f64_as_ne_bytes(buf),
}
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
match self {
DecodingBuffer::U8(buf) => buf,
DecodingBuffer::I8(buf) => bytecast::i8_as_ne_mut_bytes(buf),
DecodingBuffer::U16(buf) => bytecast::u16_as_ne_mut_bytes(buf),
DecodingBuffer::I16(buf) => bytecast::i16_as_ne_mut_bytes(buf),
@@ -197,6 +210,43 @@ impl<'a> DecodingBuffer<'a> {
DecodingBuffer::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf),
}
}
pub fn byte_len(&self) -> usize {
self.as_bytes().len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DecodingSampleType {
U8,
U16,
U32,
U64,
F16,
F32,
F64,
I8,
I16,
I32,
I64,
}
impl DecodingSampleType {
fn extent_for_bytes(self, bytes: usize) -> DecodingExtent {
match self {
DecodingSampleType::U8 => DecodingExtent::U8(bytes),
DecodingSampleType::U16 => DecodingExtent::U16(bytes.div_ceil(2)),
DecodingSampleType::U32 => DecodingExtent::U32(bytes.div_ceil(4)),
DecodingSampleType::U64 => DecodingExtent::U64(bytes.div_ceil(8)),
DecodingSampleType::I8 => DecodingExtent::I8(bytes),
DecodingSampleType::I16 => DecodingExtent::I16(bytes.div_ceil(2)),
DecodingSampleType::I32 => DecodingExtent::I32(bytes.div_ceil(4)),
DecodingSampleType::I64 => DecodingExtent::I64(bytes.div_ceil(8)),
DecodingSampleType::F16 => DecodingExtent::F16(bytes.div_ceil(2)),
DecodingSampleType::F32 => DecodingExtent::F32(bytes.div_ceil(4)),
DecodingSampleType::F64 => DecodingExtent::F64(bytes.div_ceil(8)),
}
}
}
/// Information on the byte buffer that should be supplied to the decoder.
@@ -205,11 +255,131 @@ impl<'a> DecodingBuffer<'a> {
/// caller provided buffer must fit the expectations of the decoder to be filled with data from the
/// current image.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct BufferLayoutPreference {
/// Minimum byte size of the buffer to read image data.
pub len: usize,
/// The interpretation of each sample in the image.
///
/// We only support a uniform sample layout. Detailed information for mixed colors may be added
/// in the future and will become available by explicit query. The same goes for the bit-depth
/// of samples that must also be uniform.
pub sample_format: SampleFormat,
/// The type representation for each sample. Only available for depths and formats which the
/// library can describe.
pub sample_type: Option<DecodingSampleType>,
/// Minimum number of bytes to represent a row of image data of the requested content.
pub row_stride: Option<NonZeroUsize>,
/// Number of planes in the image.
pub planes: usize,
/// Number of bytes used to represent one plane.
pub plane_stride: Option<NonZeroUsize>,
/// Number of bytes of data when reading all planes.
pub complete_len: usize,
}
impl BufferLayoutPreference {
fn from_planes(layout: &image::PlaneLayout) -> Self {
BufferLayoutPreference {
len: layout.readout.plane_stride,
row_stride: core::num::NonZeroUsize::new(layout.readout.row_stride),
planes: layout.plane_offsets.len(),
plane_stride: core::num::NonZeroUsize::new(layout.readout.plane_stride),
complete_len: layout.total_bytes,
sample_format: layout.readout.sample_format,
sample_type: Self::sample_type(layout.readout.sample_format, layout.readout.color),
}
}
fn sample_type(sample_format: SampleFormat, color: ColorType) -> Option<DecodingSampleType> {
Some(match sample_format {
SampleFormat::Uint => match color.bit_depth() {
n if n <= 8 => DecodingSampleType::U8,
n if n <= 16 => DecodingSampleType::U16,
n if n <= 32 => DecodingSampleType::U32,
n if n <= 64 => DecodingSampleType::U64,
_ => return None,
},
SampleFormat::IEEEFP => match color.bit_depth() {
16 => DecodingSampleType::F16,
32 => DecodingSampleType::F32,
64 => DecodingSampleType::F64,
_ => return None,
},
SampleFormat::Int => match color.bit_depth() {
n if n <= 8 => DecodingSampleType::I8,
n if n <= 16 => DecodingSampleType::I16,
n if n <= 32 => DecodingSampleType::I32,
n if n <= 64 => DecodingSampleType::I64,
_ => return None,
},
_other => {
return None;
}
})
}
}
impl image::ReadoutLayout {
// FIXME: when planes are not homogenous (i.e. subsampled or differing depths) then the
// `readout_for_size` or `to_plane_layout` needs a parameter to determine the planes being
// read instead of assuming a constant repeated size for them.
fn result_extent_for_planes(
self: &image::ReadoutLayout,
planes: core::ops::Range<u16>,
) -> TiffResult<DecodingExtent> {
let buffer = self.to_plane_layout()?;
// The layout is for all planes. So restrict ourselves to the planes that were requested.
let offset = match buffer.plane_offsets.get(usize::from(planes.start)) {
Some(n) => *n,
None => {
return Err(TiffError::UsageError(UsageError::InvalidPlaneIndex(
planes.start,
)))
}
};
let end = match buffer.plane_offsets.get(usize::from(planes.end)) {
Some(n) => *n,
None => buffer.total_bytes,
};
let buffer_bytes = end - offset;
let bits_per_sample = self.color.bit_depth();
let Some(sample_type) = BufferLayoutPreference::sample_type(self.sample_format, self.color)
else {
if matches!(
self.sample_format,
SampleFormat::Uint | SampleFormat::Int | SampleFormat::IEEEFP
) {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedSampleDepth(bits_per_sample),
));
} else {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedSampleFormat(vec![self.sample_format]),
));
}
};
Ok(sample_type.extent_for_bytes(buffer_bytes))
}
#[inline(always)]
fn assert_min_layout<T>(&self, buffer: &[T]) -> TiffResult<()> {
if core::mem::size_of_val(buffer) < self.plane_stride {
Err(TiffError::UsageError(
UsageError::InsufficientOutputBufferSize {
needed: self.plane_stride,
provided: buffer.len(),
},
))
} else {
Ok(())
}
}
}
/// The count and matching discriminant for a `DecodingBuffer`.
@@ -266,17 +436,19 @@ impl DecodingExtent {
.map_err(overflow)
}
fn assert_layout(self, buffer: usize) -> TiffResult<()> {
let needed_bytes = self.preferred_layout()?.size();
if buffer < needed_bytes {
Err(TiffError::UsageError(
UsageError::InsufficientOutputBufferSize {
needed: needed_bytes,
provided: buffer,
},
))
} else {
Ok(())
fn sample_type(&self) -> DecodingSampleType {
match *self {
DecodingExtent::U8(_) => DecodingSampleType::U8,
DecodingExtent::U16(_) => DecodingSampleType::U16,
DecodingExtent::U32(_) => DecodingSampleType::U32,
DecodingExtent::U64(_) => DecodingSampleType::U64,
DecodingExtent::F16(_) => DecodingSampleType::F16,
DecodingExtent::F32(_) => DecodingSampleType::F32,
DecodingExtent::F64(_) => DecodingSampleType::F64,
DecodingExtent::I8(_) => DecodingSampleType::I8,
DecodingExtent::I16(_) => DecodingSampleType::I16,
DecodingExtent::I32(_) => DecodingSampleType::I32,
DecodingExtent::I64(_) => DecodingSampleType::I64,
}
}
}
@@ -367,7 +539,7 @@ pub struct IfdDecoder<'lt> {
inner: tag_reader::TagReader<'lt, dyn tag_reader::EntryDecoder + 'lt>,
}
fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) {
fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: u16) {
fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
for i in N..buf.len() {
buf[i] = buf[i].wrapping_add(buf[i - N]);
@@ -390,6 +562,8 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) {
}
}
let samples = usize::from(samples);
match (bit_depth, samples) {
// Note we can't use `windows` or so due to the overlap between each iteration. We split
// the cases by the samples / lookback constant so that each is optimized individually.
@@ -456,7 +630,9 @@ fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) {
}
}
fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) {
fn predict_f32(input: &mut [u8], output: &mut [u8], samples: u16) {
let samples = usize::from(samples);
for i in samples..input.len() {
input[i] = input[i].wrapping_add(input[i - samples]);
}
@@ -471,7 +647,9 @@ fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) {
}
}
fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) {
fn predict_f16(input: &mut [u8], output: &mut [u8], samples: u16) {
let samples = usize::from(samples);
for i in samples..input.len() {
input[i] = input[i].wrapping_add(input[i - samples]);
}
@@ -484,7 +662,9 @@ fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) {
}
}
fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) {
fn predict_f64(input: &mut [u8], output: &mut [u8], samples: u16) {
let samples = usize::from(samples);
for i in samples..input.len() {
input[i] = input[i].wrapping_add(input[i - samples]);
}
@@ -506,7 +686,7 @@ fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) {
fn fix_endianness_and_predict(
buf: &mut [u8],
bit_depth: u8,
samples: usize,
samples: u16,
byte_order: ByteOrder,
predictor: Predictor,
) {
@@ -667,6 +847,8 @@ impl<R: Read + Seek> Decoder<R> {
height: 0,
bits_per_sample: 1,
samples: 1,
extra_samples: vec![],
photometric_samples: 1,
sample_format: SampleFormat::Uint,
photometric_interpretation: PhotometricInterpretation::BlackIsZero,
compression_method: CompressionMethod::None,
@@ -678,6 +860,7 @@ impl<R: Read + Seek> Decoder<R> {
tile_attributes: None,
chunk_offsets: Vec::new(),
chunk_bytes: Vec::new(),
chroma_subsampling: (2, 2),
},
};
decoder.next_image()?;
@@ -975,107 +1158,21 @@ impl<R: Read + Seek> Decoder<R> {
Ok(u32::try_from(self.image().chunk_offsets.len())?)
}
pub fn read_chunk_to_buffer(
&mut self,
mut buffer: DecodingBuffer,
chunk_index: u32,
output_width: usize,
) -> TiffResult<()> {
let output_row_stride = (output_width as u64)
.saturating_mul(self.image.samples_per_pixel() as u64)
.saturating_mul(self.image.bits_per_sample as u64)
.div_ceil(8);
self.read_chunk_to_bytes(buffer.as_bytes_mut(), chunk_index, output_row_stride)
}
fn read_chunk_to_bytes(
&mut self,
buffer: &mut [u8],
chunk_index: u32,
output_row_stride: u64,
layout: &image::ReadoutLayout,
) -> TiffResult<()> {
let offset = self.image.chunk_file_range(chunk_index)?.0;
self.goto_offset_u64(offset)?;
self.image.expand_chunk(
&mut self.value_reader,
buffer,
output_row_stride.try_into()?,
chunk_index,
)?;
self.image
.expand_chunk(&mut self.value_reader, buffer, layout, chunk_index)?;
Ok(())
}
fn result_buffer(&self, width: usize, height: usize) -> TiffResult<DecodingResult> {
self.result_extent(width, height)?
.to_result_buffer(&self.value_reader.limits)
}
// FIXME: it's unusual for this method to take a `usize` dimension parameter since those would
// typically come from the image data. The preferred consistent representation should be `u32`
// and that would avoid some unusual casts `usize -> u64` with a `u64::from` instead.
fn result_extent(&self, width: usize, height: usize) -> TiffResult<DecodingExtent> {
let bits_per_sample = self.image.bits_per_sample;
// If samples take up more than one byte, we expand it to a full number (integer or float)
// and the number of samples in that integer type just counts the number in the underlying
// image itself. Otherwise, a row is represented as the byte slice of bitfields without
// expansion.
let row_samples = if bits_per_sample >= 8 {
width
} else {
((width as u64) * bits_per_sample as u64)
.div_ceil(8)
.try_into()
.map_err(|_| TiffError::LimitsExceeded)?
};
let buffer_size = row_samples
.checked_mul(height)
.and_then(|x| x.checked_mul(self.image.samples_per_pixel()))
.ok_or(TiffError::LimitsExceeded)?;
Ok(match self.image().sample_format {
SampleFormat::Uint => match bits_per_sample {
n if n <= 8 => DecodingExtent::U8(buffer_size),
n if n <= 16 => DecodingExtent::U16(buffer_size),
n if n <= 32 => DecodingExtent::U32(buffer_size),
n if n <= 64 => DecodingExtent::U64(buffer_size),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
SampleFormat::IEEEFP => match bits_per_sample {
16 => DecodingExtent::F16(buffer_size),
32 => DecodingExtent::F32(buffer_size),
64 => DecodingExtent::F64(buffer_size),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
SampleFormat::Int => match bits_per_sample {
n if n <= 8 => DecodingExtent::I8(buffer_size),
n if n <= 16 => DecodingExtent::I16(buffer_size),
n if n <= 32 => DecodingExtent::I32(buffer_size),
n if n <= 64 => DecodingExtent::I64(buffer_size),
n => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedBitsPerChannel(n),
))
}
},
format => {
return Err(TiffUnsupportedError::UnsupportedSampleFormat(vec![format]).into())
}
})
}
/// Returns the layout preferred to read the specified chunk with [`Self::read_chunk_bytes`].
///
/// Returns the layout without being specific as to the underlying type for forward
@@ -1083,41 +1180,78 @@ impl<R: Read + Seek> Decoder<R> {
/// channels of individual *bit* length and format each.
///
/// See [`Self::colortype`] to describe the sample types.
///
/// # Bugs
///
/// When the image is stored as a planar configuration, this method will currently only
/// indicate the layout needed to read the first data plane. This will be fixed in a future
/// major version of `tiff`.
pub fn image_chunk_buffer_layout(
&mut self,
chunk_index: u32,
) -> TiffResult<BufferLayoutPreference> {
let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
let layout = self
.result_extent(data_dims.0 as usize, data_dims.1 as usize)?
.preferred_layout()?;
let readout = self.image().readout_for_size(data_dims.0, data_dims.1)?;
let row_stride = self.image.minimum_row_stride(data_dims);
let extent = readout.result_extent_for_planes(0..1)?;
let sample_type = extent.sample_type();
let layout = extent.preferred_layout()?;
let row_stride = core::num::NonZeroUsize::new(readout.minimum_row_stride);
let plane_stride = core::num::NonZeroUsize::new(readout.plane_stride);
Ok(BufferLayoutPreference {
len: layout.size(),
row_stride,
planes: 1,
plane_stride,
complete_len: layout.size(),
sample_format: self.image().sample_format,
sample_type: Some(sample_type),
})
}
/// Return the layout preferred to read several planes corresponding to the specified region.
///
/// This is similar to [`Self::image_chunk_buffer_layout`] but can read chunks from all planes
/// at the corresponding coordinates of the image.
///
/// # Bugs
///
/// Sub-sampled images are not yet supported properly.
pub fn image_coding_unit_layout(
&mut self,
code_unit: TiffCodingUnit,
) -> TiffResult<BufferLayoutPreference> {
match self.image().planar_config {
PlanarConfiguration::Chunky => return self.image_chunk_buffer_layout(code_unit.0),
PlanarConfiguration::Planar => {}
}
let (width, height) = self.image().chunk_data_dimensions(code_unit.0)?;
let layout = self
.image()
.readout_for_size(width, height)?
.to_plane_layout()?;
if code_unit.0 >= layout.readout.chunks_per_plane {
return Err(TiffError::UsageError(UsageError::InvalidCodingUnit(
code_unit.0,
layout.readout.chunks_per_plane,
)));
}
Ok(BufferLayoutPreference::from_planes(&layout))
}
/// Read the specified chunk (at index `chunk_index`) and return the binary data as a Vector.
///
/// # Bugs
///
/// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`.
/// Note that for planar images each chunk contains only one sample of the underlying data.
pub fn read_chunk(&mut self, chunk_index: u32) -> TiffResult<DecodingResult> {
let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let mut result = self.result_buffer(data_dims.0 as usize, data_dims.1 as usize)?;
let readout = self.image().readout_for_size(width, height)?;
self.read_chunk_to_buffer(result.as_buffer(0), chunk_index, data_dims.0 as usize)?;
let mut result = readout
.result_extent_for_planes(0..1)?
.to_result_buffer(&self.value_reader.limits)?;
self.read_chunk_to_bytes(result.as_buffer(0).as_bytes_mut(), chunk_index, &readout)?;
Ok(result)
}
@@ -1128,22 +1262,95 @@ impl<R: Read + Seek> Decoder<R> {
/// call to [`Self::image_chunk_buffer_layout`]. Note that the alignment may be arbitrary, but
/// an alignment smaller than the preferred alignment may perform worse.
///
/// Note that for planar images each chunk contains only one sample of the underlying data.
pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> {
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let layout = self.image().readout_for_size(width, height)?;
layout.assert_min_layout(buffer)?;
self.read_chunk_to_bytes(buffer, chunk_index, &layout)?;
Ok(())
}
/// Read the specified chunk (at index `chunk_index`) into a provide buffer.
///
/// It will re-allocate the buffer into the correct type and size, within the decoder's
/// configured limits, and then pass it to the underlying method. This is essentially a
/// type-safe wrapper around the raw [`Self::read_chunk_bytes`] method.
///
/// Note that for planar images each chunk contains only one sample of the underlying data.
pub fn read_chunk_to_buffer(
&mut self,
buffer: &mut DecodingResult,
chunk_index: u32,
output_width: usize,
) -> TiffResult<()> {
let (width, height) = self.image().chunk_data_dimensions(chunk_index)?;
let mut layout = self.image().readout_for_size(width, height)?;
layout.set_row_stride(output_width)?;
let extent = layout.result_extent_for_planes(0..1)?;
buffer.resize_to_extent(extent, &self.value_reader.limits)?;
self.read_chunk_to_bytes(buffer.as_buffer(0).as_bytes_mut(), chunk_index, &layout)?;
Ok(())
}
/// Read chunks corresponding to several planes of a region of pixels.
///
/// For non planar images this is equivalent to [`Self::read_chunk_bytes`] as there is only one
/// plane in the image. For planar images the planes are stored consecutively into the output
/// buffer. Returns an error if not enough space for at least one plane is provided. Otherwise
/// reads all planes that can be stored completely in the provided output buffer.
///
/// A region is a rectangular assortment of pixels in the image, depending on the chunk type
/// either strips or tiles. Borrowing terminology from JPEG we call the collection of all
/// chunks from all planes that encode samples from the same region a "coding unit".
///
/// # Bugs
///
/// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`.
pub fn read_chunk_bytes(&mut self, chunk_index: u32, buffer: &mut [u8]) -> TiffResult<()> {
let data_dims = self.image().chunk_data_dimensions(chunk_index)?;
/// Sub-sampled images are not yet supported properly.
pub fn read_coding_unit_bytes(
&mut self,
slice: TiffCodingUnit,
buffer: &mut [u8],
) -> TiffResult<()> {
let (width, height) = self.image().chunk_data_dimensions(slice.0)?;
let readout = self.image().readout_for_size(width, height)?;
let extent = self.result_extent(data_dims.0 as usize, data_dims.1 as usize)?;
extent.assert_layout(buffer.len())?;
let ref layout @ image::PlaneLayout {
ref plane_offsets,
// We assume that is correct, so really it can be ignored.
total_bytes: _,
ref readout,
} = readout.to_plane_layout()?;
let output_row_stride = u64::from(data_dims.0)
.saturating_mul(self.image.samples_per_pixel() as u64)
.saturating_mul(self.image.bits_per_sample as u64)
.div_ceil(8);
if slice.0 >= readout.chunks_per_plane {
return Err(TiffError::UsageError(UsageError::InvalidCodingUnit(
slice.0,
readout.chunks_per_plane,
)));
}
self.read_chunk_to_bytes(buffer, chunk_index, output_row_stride)?;
// No subsamples planes support, for now.
let used_plane_offsets = usize::from(layout.used_planes(buffer)?);
debug_assert!(used_plane_offsets >= 1, "Should have errored");
for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() {
let chunk = slice.0 + idx as u32 * readout.chunks_per_plane;
self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?;
self.image.expand_chunk(
&mut self.value_reader,
&mut buffer[plane_offset..],
readout,
chunk,
)?;
}
Ok(())
}
@@ -1176,111 +1383,121 @@ impl<R: Read + Seek> Decoder<R> {
/// indicate the layout needed to read the first data plane. This will be fixed in a future
/// major version of `tiff`.
pub fn image_buffer_layout(&mut self) -> TiffResult<BufferLayoutPreference> {
let data_dims @ (width, height) = (self.image.width, self.image.height);
let layout = self
.result_extent(width as usize, height as usize)?
.preferred_layout()?;
let row_stride = self.image.minimum_row_stride(data_dims);
Ok(BufferLayoutPreference {
len: layout.size(),
row_stride,
})
let layout = self.image().readout_for_image()?.to_plane_layout()?;
Ok(BufferLayoutPreference::from_planes(&layout))
}
/// Decodes the entire image and return it as a Vector
///
/// # Examples
///
/// This method is deprecated. For replacement usage see `examples/decode.rs`.
///
/// # Bugs
///
/// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`.
///
/// # Intent to deprecate
///
/// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving
/// the buffer across multiple calls. This old method will likely keep its bugged planar
/// behavior until it is fully replaced, to ensure that existing code will not run into
/// unexpectedly large allocations that will error on limits instead.
pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let width = self.image().width;
let height = self.image().height;
let readout = self.image().readout_for_image()?;
let mut result = readout
.result_extent_for_planes(0..1)?
.to_result_buffer(&self.value_reader.limits)?;
let mut result = self.result_buffer(width as usize, height as usize)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
Ok(result)
}
/// Decodes the entire image into a provided buffer.
///
/// It will re-allocate the buffer into the correct type and size, within the decoder's
/// configured limits, and then pass it to the underlying method. This is essentially a
/// type-safe wrapper around the raw [`Self::read_image_bytes`] method.
///
/// # Examples
///
/// ```
/// use tiff::decoder::{Decoder, DecodingResult, Limits};
///
/// let mut result = DecodingResult::I8(vec![]);
///
/// let mut reader = /* */
/// # Decoder::new(std::io::Cursor::new(include_bytes!(concat!(
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
/// # )))).unwrap();
///
/// reader.read_image_to_buffer(&mut result)?;
///
/// # Ok::<_, tiff::TiffError>(())
/// ```
pub fn read_image_to_buffer(
&mut self,
result: &mut DecodingResult,
) -> TiffResult<BufferLayoutPreference> {
let readout = self.image().readout_for_image()?;
let planes = readout.to_plane_layout()?;
let layout = BufferLayoutPreference::from_planes(&planes);
let extent = readout.result_extent_for_planes(0..1)?;
result.resize_to_extent(extent, &self.value_reader.limits)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
Ok(layout)
}
/// Decodes the entire image into a provided buffer.
///
/// Returns a [`TiffError::UsageError`] if the chunk is smaller than the size indicated with a
/// call to [`Self::image_buffer_layout`]. Note that the alignment may be arbitrary, but an
/// alignment smaller than the preferred alignment may perform worse.
///
/// # Bugs
/// # Error
///
/// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`.
/// Returns an error if the buffer fits less than one plane. In particular, for non-planar
/// images returns an error if the buffer does not fit the required size.
pub fn read_image_bytes(&mut self, buffer: &mut [u8]) -> TiffResult<()> {
let width = self.image().width;
let height = self.image().height;
let readout = self.image().readout_for_image()?;
let extent = self.result_extent(width as usize, height as usize)?;
extent.assert_layout(buffer.len())?;
let ref layout @ image::PlaneLayout {
ref plane_offsets,
// We assume that is correct, so really it can be ignored.
total_bytes: _,
ref readout,
} = readout.to_plane_layout()?;
if width == 0 || height == 0 {
return Ok(());
}
let used_plane_offsets = usize::from(layout.used_planes(buffer)?);
debug_assert!(used_plane_offsets >= 1, "Should have errored");
let chunk_dimensions = self.image().chunk_dimensions()?;
let chunk_dimensions = (
chunk_dimensions.0.min(width),
chunk_dimensions.1.min(height),
);
if chunk_dimensions.0 == 0 || chunk_dimensions.1 == 0 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
let samples = self.image().samples_per_pixel();
if samples == 0 {
return Err(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
));
}
let output_row_bits = (width as u64 * self.image.bits_per_sample as u64)
.checked_mul(samples as u64)
.ok_or(TiffError::LimitsExceeded)?;
let output_row_stride: usize = output_row_bits.div_ceil(8).try_into()?;
let chunk_row_bits = (chunk_dimensions.0 as u64 * self.image.bits_per_sample as u64)
.checked_mul(samples as u64)
.ok_or(TiffError::LimitsExceeded)?;
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
let chunks_across = ((width - 1) / chunk_dimensions.0 + 1) as usize;
if chunks_across > 1 && chunk_row_bits % 8 != 0 {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::MisalignedTileBoundaries,
));
}
let image_chunks = self.image().chunk_offsets.len() / self.image().strips_per_pixel();
// For multi-band images, only the first band is read.
// Possible improvements:
// * pass requested band as parameter
// * collect bands to a RGB encoding result in case of RGB bands
for chunk in 0..image_chunks {
self.goto_offset_u64(self.image().chunk_offsets[chunk])?;
for chunk in 0..readout.chunks_per_plane {
let x = (chunk % readout.chunks_across) as usize;
let y = (chunk / readout.chunks_across) as usize;
let x = chunk % chunks_across;
let y = chunk / chunks_across;
let buffer_offset =
y * output_row_stride * chunk_dimensions.1 as usize + x * chunk_row_bytes;
self.image.expand_chunk(
&mut self.value_reader,
&mut buffer[buffer_offset..],
output_row_stride,
chunk as u32,
)?;
let buffer_offset = y * readout.chunk_col_stride + x * readout.chunk_row_stride;
for (idx, &plane_offset) in plane_offsets[..used_plane_offsets].iter().enumerate() {
let chunk = chunk + idx as u32 * readout.chunks_per_plane;
self.goto_offset_u64(self.image().chunk_offsets[chunk as usize])?;
self.image.expand_chunk(
&mut self.value_reader,
&mut buffer[plane_offset..][buffer_offset..],
readout,
chunk,
)?;
}
}
Ok(())
+66 -4
View File
@@ -11,7 +11,10 @@ use std::{
use crate::{
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
tags::{
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
SampleFormat, Tag, Type,
},
Directory, TiffError, TiffFormatError,
};
@@ -506,6 +509,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
row_samples: u64,
width: u32,
height: u32,
extra_samples: Vec<u16>,
rows_per_strip: u64,
strip_offsets: Vec<K::OffsetType>,
strip_byte_count: Vec<K::OffsetType>,
@@ -562,11 +566,14 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
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();
encoder.write_tag(Tag::SampleFormat, &sample_format[..])?;
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
// The default for this tag is 2,2 for subsampling but we do not support such a
// transformation. Instead all samples must be provided.
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
}
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
encoder.write_tag(
@@ -583,6 +590,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
strip_idx: 0,
row_samples,
rows_per_strip,
extra_samples: vec![],
width,
height,
strip_offsets: Vec::new(),
@@ -594,6 +602,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
})
}
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
if self.strip_idx != 0 {
return Err(TiffError::UsageError(
UsageError::ReconfiguredAfterImageWrite,
));
}
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
self.extra_samples
.extend(extra.iter().map(ExtraSamples::to_u16));
self.encoder
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
self.row_samples = row_samples;
Ok(())
}
/// Number of samples the next strip should have.
pub fn next_strip_sample_count(&self) -> u64 {
if self.strip_idx >= self.strip_count {
@@ -731,6 +760,39 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
}
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
if self.extra_samples.is_empty() {
self.encoder
.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
} else {
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
let replicated =
core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
sample_format.extend(replicated);
self.encoder
.write_tag(Tag::BitsPerSample, &sample_format[..])?;
self.encoder.write_tag(
Tag::SamplesPerPixel,
u16::try_from(<T>::BITS_PER_SAMPLE.len() + self.extra_samples.len())?,
)?;
}
let mut sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
let extra_format = sample_format
.first()
.copied()
// Frankly should not occur, we have no sample format without at least one entry. We
// would need an interface upgrade to handle this however. Either decide to support
// heterogeneous sample formats or a way to provide fallback that is used for purely
// extra samples and not provided via a slice used for samples themselves.
.unwrap_or(SampleFormat::Void.to_u16());
sample_format.extend(core::iter::repeat_n(extra_format, self.extra_samples.len()));
self.encoder
.write_tag(Tag::SampleFormat, &sample_format[..])?;
self.encoder
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
self.encoder.write_tag(
+37
View File
@@ -5,6 +5,14 @@ use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult};
use super::writer::TiffWriter;
/// Trait for types that can be encoded in a tiff file
#[diagnostic::on_unimplemented(
message = "the type `{Self}` does not implement `TiffValue`",
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
note = "the trait is implemented for shared references to values",
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
note = "the trait is implemented for arrays if it is implemented for a slice",
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
)]
pub trait TiffValue {
const BYTE_LEN: u8;
const FIELD_TYPE: Type;
@@ -450,6 +458,14 @@ impl TiffValue for str {
}
}
// If you pass `&Vec<_>` then you'd get at first sight the complaint:
//
// `Vec<T>` does not implement `TiffValue`
//
// and a list of implementations of `TiffValue` that are quite unrelated. That is not very helpful.
// We do not *want* `Vec` to implement the trait as an owning type, instead you should pass a
// reference to a slice. The error message is further customized in the diagnostics of the trait.
#[diagnostic::do_not_recommend]
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
const BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -467,6 +483,27 @@ impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
}
}
/// An array is treated like a slice of the same length.
impl<T, const N: usize> TiffValue for [T; N]
where
[T]: TiffValue,
{
const BYTE_LEN: u8 = <[T]>::BYTE_LEN;
const FIELD_TYPE: Type = <[T]>::FIELD_TYPE;
fn count(&self) -> usize {
self.as_slice().count()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
self.as_slice().write(writer)
}
fn data(&self) -> Cow<'_, [u8]> {
self.as_slice().data()
}
}
macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] {
+8 -2
View File
@@ -221,6 +221,8 @@ fn encoder_on_short_writes() {
};
let imgdata = vec![42u8; 100 * 100];
let mut data = crate::decoder::DecodingResult::U8(vec![]);
for comp in [
super::Compression::Uncompressed,
super::Compression::Packbits,
@@ -240,8 +242,12 @@ fn encoder_on_short_writes() {
write.inner.set_position(0);
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
let data = tiff_reader.read_image().unwrap();
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
assert!(matches!(data, crate::decoder::DecodingResult::U8(v) if v == imgdata));
assert!(
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
"{data:?} @ {}",
data.as_buffer(0).byte_len()
);
}
}
+18
View File
@@ -90,6 +90,9 @@ quick_error! {
InvalidTypeForTag {
display("tag has invalid type")
}
InvalidCountForTag(tag: Tag, len: usize) {
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
}
StripTileTagConflict {
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
}
@@ -156,6 +159,9 @@ quick_error! {
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
display("unsupported photometric interpretation \"{interpretation:?}\"")
}
ChromaSubsampling {
display("chroma subsampling of YCbCr color is unsupported")
}
MisalignedTileBoundaries {
display("tile rows are not aligned to byte boundaries")
}
@@ -175,6 +181,12 @@ quick_error! {
InvalidChunkIndex(index: u32) {
display("invalid chunk index ({index}) requested")
}
InvalidPlaneIndex(index: u16) {
display("invalid plane index ({index}) requested")
}
InvalidCodingUnit(index: u32, have: u32) {
display("out of bounds coding unit ({index}) requested, only {have} available")
}
PredictorCompressionMismatch {
display("requested predictor is not compatible with the requested compression")
}
@@ -187,9 +199,15 @@ quick_error! {
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
InsufficientOutputRowStride { needed: usize, requested: usize } {
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
}
ZeroIfdPointer {
display("the offset 0 can not point to a valid IFD")
}
ReconfiguredAfterImageWrite {
display("attempted to reconfigure the encoder after image writing has started")
}
}
}
+19
View File
@@ -44,6 +44,9 @@ pub enum ColorType {
/// Pixel is YCbCr
YCbCr(u8),
/// Pixel is CIE L*a*b* (ICC LAb)
Lab(u8),
/// Pixel has multiple bands/channels
Multiband { bit_depth: u8, num_samples: u16 },
}
@@ -58,7 +61,23 @@ impl ColorType {
| ColorType::CMYK(b)
| ColorType::CMYKA(b)
| ColorType::YCbCr(b)
| ColorType::Lab(b)
| ColorType::Multiband { bit_depth: b, .. } => b,
}
}
fn num_samples(&self) -> u16 {
match *self {
ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3,
ColorType::Palette(_) => 1,
ColorType::GrayA(_) => 2,
ColorType::RGBA(_) => 4,
ColorType::CMYK(_) => 4,
ColorType::CMYKA(_) => 5,
ColorType::YCbCr(_) => 3,
ColorType::Lab(_) => 3,
ColorType::Multiband { num_samples, .. } => num_samples,
}
}
}
+31 -7
View File
@@ -20,7 +20,7 @@ macro_rules! tags {
impl $name {
#[inline(always)]
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
match n {
$( $val => Ok($name::$tag), )*
n => Err(n),
@@ -28,7 +28,7 @@ macro_rules! tags {
}
#[inline(always)]
fn __to_inner_type(&self) -> $ty {
const fn __to_inner_type(&self) -> $ty {
match *self {
$( $name::$tag => $val, )*
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
@@ -42,19 +42,25 @@ macro_rules! tags {
($name:tt, u16, $($unknown_doc:ident)*) => {
impl $name {
#[inline(always)]
pub fn from_u16(val: u16) -> Option<Self> {
Self::__from_inner_type(val).ok()
pub const fn from_u16(val: u16) -> Option<Self> {
match Self::__from_inner_type(val) {
Ok(v) => Some(v),
Err(_) => None,
}
}
$(
#[inline(always)]
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
match Self::__from_inner_type($unknown_doc) {
Ok(v) => v,
Err(_) => $name::Unknown($unknown_doc),
}
}
)*
#[inline(always)]
pub fn to_u16(&self) -> u16 {
pub const fn to_u16(&self) -> u16 {
Self::__to_inner_type(self)
}
}
@@ -122,6 +128,11 @@ pub enum Tag(u16) unknown(
SMaxSampleValue = 341, // TODO add support
// JPEG
JPEGTables = 347,
// Subsampling
#[doc(alias = "YCbCrSubsampling")]
ChromaSubsampling = 530, // TODO add support
#[doc(alias = "YCbCrPositioning")]
ChromaPositioning = 531, // TODO add support
// GeoTIFF
ModelPixelScaleTag = 33550, // (SoftDesk)
ModelTransformationTag = 34264, // (JPL Carto Group)
@@ -266,6 +277,8 @@ pub enum PhotometricInterpretation(u16) {
CMYK = 5,
YCbCr = 6,
CIELab = 8,
IccLab = 9,
ItuLab = 10,
}
}
@@ -311,6 +324,17 @@ pub enum SampleFormat(u16) unknown(
}
}
tags! {
pub enum ExtraSamples(u16) {
/// There is no specified association between the sample and the image.
Unspecified = 0,
/// The sample is associated alpha, i.e. pre-multiplied color.
AssociatedAlpha = 1,
/// The sample is unassociated alpha such as a mask. There might be more than one such sample.
UnassociatedAlpha = 2,
}
}
/// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder {
+36
View File
@@ -684,3 +684,39 @@ fn test_fax4() {
fn test_fax4_white_is_min() {
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
}
#[test]
fn write_extra_bits_image() {
use tiff::{encoder, tags::ExtraSamples};
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
let img_file = File::create(path).expect("Cannot find test image!");
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
// all pixels are supposed to be 0, the extra is 1
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
img.write_data(&data)
.expect("correct amount of data to write");
}
#[test]
fn extra_bits_gray() {
test_image_sum_u8(
"extra_bits_gray_8b.tiff",
ColorType::Multiband {
bit_depth: 8,
num_samples: 2,
},
64,
);
}
#[test]
fn extra_bits_rgb() {
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
}
+76
View File
@@ -0,0 +1,76 @@
use tiff::decoder::{Decoder, TiffCodingUnit};
use std::fs::File;
use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/images/";
#[test]
fn read_all_planes() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_buffer_layout().unwrap();
assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
eprintln!("{:?}", layout);
let mut buffer = vec![0u8; layout.complete_len];
tif.read_image_bytes(&mut buffer).unwrap();
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
// depend on the file itself that is being used.
const PLANE_EXCPECTED_SUMS: [u32; 3] = [15417630, 13007788, 11103530];
for (plane, expected) in (0..layout.planes).zip(PLANE_EXCPECTED_SUMS) {
let plane_data = &buffer[plane_stride * plane..][..layout.len];
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
assert_eq!(sum, expected);
}
}
#[test]
fn read_first_coding_unit() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut tif = Decoder::new(io).unwrap();
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
assert_eq!(layout.planes, 3);
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
eprintln!("{:?}", layout);
let mut buffer = vec![0u8; layout.complete_len];
struct Expected {
plane_sums: [u32; 3],
unit: TiffCodingUnit,
}
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
// depend on the file itself that is being used. Also this varies by what unit we are reading.
const PLANE_EXCPECTED_SUMS: [Expected; 2] = [
Expected {
plane_sums: [856853, 767686, 688532],
unit: TiffCodingUnit(0),
},
Expected {
plane_sums: [883679, 797329, 711086],
unit: TiffCodingUnit(1),
},
];
for expected in PLANE_EXCPECTED_SUMS {
tif.read_coding_unit_bytes(expected.unit, &mut buffer)
.unwrap();
for (plane, expected) in (0..layout.planes).zip(expected.plane_sums) {
let plane_data = &buffer[plane_stride * plane..][..layout.len];
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
assert_eq!(sum, expected);
}
}
}
+17 -6
View File
@@ -57,7 +57,7 @@ fn encode_decode() {
),
(Tag::Compression, ifd::Value::Short(1)),
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
@@ -81,7 +81,10 @@ fn encode_decode() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -161,7 +164,9 @@ fn encode_decode_big() {
decoder.get_tag(Tag::Artist).unwrap(),
ifd::Value::Ascii("Image-tiff".into())
);
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::U8(img_res) = buffer {
assert_eq!(image_data, img_res);
} else {
panic!("Wrong data type");
@@ -235,6 +240,7 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
macro_rules! test_roundtrip {
($name:ident, $buffer:ident, $buffer_ty:ty) => {
#[track_caller]
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
file: &str,
expected_type: ColorType,
@@ -244,7 +250,10 @@ macro_rules! test_roundtrip {
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() {
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
decoder.read_image_to_buffer(&mut buffer).unwrap();
let image_data = match buffer {
DecodingResult::$buffer(res) => res,
_ => panic!("Wrong data type"),
};
@@ -258,9 +267,11 @@ macro_rules! test_roundtrip {
}
file.seek(SeekFrom::Start(0)).unwrap();
{
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
let mut decoder = Decoder::new(&mut file).unwrap();
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
assert_eq!(image_data, img_res);
decoder.read_image_to_buffer(&mut buffer).unwrap();
if let DecodingResult::$buffer(img_res) = buffer {
assert_eq!(*image_data, img_res);
} else {
panic!("Wrong data type");
}
Binary file not shown.
Binary file not shown.
Binary file not shown.