Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e41159ce05 | ||
|
|
03979faef8 | ||
|
|
857973cb21 | ||
|
|
cef21c47fb | ||
|
|
a0191ed1a7 | ||
|
|
99a7b8cba7 | ||
|
|
418ea182c3 | ||
|
|
529fa64e13 | ||
|
|
3d6b94b65a | ||
|
|
a51b23091e | ||
|
|
ac65bd8477 | ||
|
|
1bd6293857 | ||
|
|
87341f9075 | ||
|
|
43343f64cd | ||
|
|
be04326264 | ||
|
|
93a68f52f5 | ||
|
|
5f329840c8 | ||
|
|
1d138a5568 | ||
|
|
9a23090b6e | ||
|
|
9d409433e8 | ||
|
|
cf680424d9 | ||
|
|
d3b777c961 | ||
|
|
24b008a7c3 | ||
|
|
34bd1da99e | ||
|
|
cfff04c97d | ||
|
|
915cc20945 | ||
|
|
74b2857364 | ||
|
|
1dab89f10e | ||
|
|
45f617b9f5 | ||
|
|
170bd3697c | ||
|
|
855eb4ef23 | ||
|
|
fee650d7f6 | ||
|
|
ee8d13249e | ||
|
|
169a896851 | ||
|
|
082a35adb9 | ||
|
|
923df5f0bd | ||
|
|
c4328644b8 | ||
|
|
27bfcf5562 | ||
|
|
7d498a0284 | ||
|
|
5b21bcc75c | ||
|
|
fe305ec24a | ||
|
|
a3ab252168 | ||
|
|
bd64e438a4 | ||
|
|
fa225e820b | ||
|
|
ea373bae00 | ||
|
|
23d1867f75 | ||
|
|
234b2ba677 | ||
|
|
a63a01319c | ||
|
|
efd2a6dbd8 | ||
|
|
d38fba4268 | ||
|
|
1732481b01 | ||
|
|
355ecc8ad9 | ||
|
|
985f3822c8 | ||
|
|
f6d95249dd |
No files matched your search
@@ -9,14 +9,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust: ["1.74.0", stable, beta, nightly]
|
rust: ["1.85.0", stable, beta, nightly]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- 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
|
- 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
|
run: cargo -Zminimal-versions generate-lockfile
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: build
|
- name: build
|
||||||
run: cargo build -v
|
run: cargo build -v
|
||||||
- name: test
|
- name: test
|
||||||
if: ${{ matrix.rust != '1.74.0' }}
|
if: ${{ matrix.rust != '1.85.0' }}
|
||||||
run: cargo test -v && cargo doc -v
|
run: cargo test -v && cargo doc -v
|
||||||
|
|
||||||
features:
|
features:
|
||||||
|
|||||||
+99
-1
@@ -1,4 +1,63 @@
|
|||||||
# Version 0.10.4 (unreleased)
|
# Version 0.11.3
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
|
||||||
|
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
|
||||||
|
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
|
||||||
|
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- A new example `tiff-ls` demonstrates use of the new features.
|
||||||
|
|
||||||
|
# Version 0.11.2
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
|
||||||
|
limits allow for such a large buffer size. Previously only the first plane
|
||||||
|
was read and the decoder will attempt a fallback to this behavior if the
|
||||||
|
limits do not allow all planes to be allocated.
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
|
||||||
|
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
|
||||||
|
exactly matched the required size.
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
|
||||||
|
promises that we only have homogeneous color depths in this major version.
|
||||||
|
|
||||||
|
# Version 0.11.1
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- The enumeration types in `tags` are now all marked with a representation of
|
||||||
|
their underlying TIFF type (e.g. `repr(u16)`) and variants are explicitly
|
||||||
|
assigned their corresponding values. That is you may _read_ the raw
|
||||||
|
discriminant and interpret that as the value—except for `Unknown` variants.
|
||||||
|
- The variants `RationalBig` and `SRationalBig` of `decoder::ifd::Value` are
|
||||||
|
deprecated as they have no corresponding TIFF type and were not constructed
|
||||||
|
during decoding.
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
- Types in `tags` now generally implement `TiffValue` and can be handed to the
|
||||||
|
`DirectoryEncoder::write_tag` method. Unlike primitive types they do _not_
|
||||||
|
always implement the trait for slices of themselves.
|
||||||
|
- Added `tags::ValueBuffer`, a byte-based buffer for tags with a runtime value
|
||||||
|
type and count.
|
||||||
|
- Added `IfdDecoder::find_tag_{buf,bytes}` to read tag values into a
|
||||||
|
`ValueBuffer` or byte slice respectively without interpreting them
|
||||||
|
immediately within the Rust type system.
|
||||||
|
- Added `DirectoryEncoder::write_{data,tag}_buf` that takes a `ValueBuffer`
|
||||||
|
instead of a statically typed argument.
|
||||||
|
- Added `DirectoryEncoder::write_entry{,_buf,_bytes}` that encode their
|
||||||
|
argument into the file and return an `Entry` for later use, without
|
||||||
|
immediately adding a tag-entry pair to the directory.
|
||||||
|
- The `TiffValue` trait is now implemented for several builtin `tags::*` types
|
||||||
|
or slices of them as appropriate for their use as values of their intended
|
||||||
|
TIFF tag.
|
||||||
|
- The `TiffValue` trait is now implemented for `[T; N]` for all types that
|
||||||
|
implement it for slices.
|
||||||
|
|
||||||
|
# Version 0.11.0
|
||||||
|
|
||||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||||
|
|
||||||
@@ -10,6 +69,45 @@ Changes:
|
|||||||
as unspecified relation. Previously, these may have been interpreted as
|
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
|
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
|
||||||
interpretation of RGB).
|
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
|
# Version 0.10.3
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -1,11 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tiff"
|
name = "tiff"
|
||||||
version = "0.10.3"
|
version = "0.11.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||||
rust-version = "1.74.0"
|
rust-version = "1.85.0"
|
||||||
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "TIFF decoding and encoding library in pure Rust"
|
description = "TIFF decoding and encoding library in pure Rust"
|
||||||
@@ -14,7 +14,7 @@ authors = ["The image-rs Developers"]
|
|||||||
repository = "https://github.com/image-rs/image-tiff"
|
repository = "https://github.com/image-rs/image-tiff"
|
||||||
categories = ["multimedia::images", "multimedia::encoding"]
|
categories = ["multimedia::images", "multimedia::encoding"]
|
||||||
|
|
||||||
exclude = ["tests/images/*", "tests/fuzz_images/*"]
|
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
half = { version = "2.4.1" }
|
half = { version = "2.4.1" }
|
||||||
@@ -26,6 +26,7 @@ flate2 = { version = "1.0.20", optional = true }
|
|||||||
weezl = { version = "0.1.10", optional = true }
|
weezl = { version = "0.1.10", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
zune-jpeg = { version = "0.5.1", optional = true }
|
zune-jpeg = { version = "0.5.1", optional = true }
|
||||||
|
image-webp = {version = "0.2.4", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3.1"
|
criterion = "0.3.1"
|
||||||
@@ -40,6 +41,7 @@ deflate = ["dep:flate2"]
|
|||||||
fax = ["dep:fax34"]
|
fax = ["dep:fax34"]
|
||||||
jpeg = ["dep:zune-jpeg"]
|
jpeg = ["dep:zune-jpeg"]
|
||||||
lzw = ["dep:weezl"]
|
lzw = ["dep:weezl"]
|
||||||
|
webp = ["dep:image-webp"]
|
||||||
zstd = ["dep:zstd"]
|
zstd = ["dep:zstd"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
|
|||||||
+104
-1
@@ -37,6 +37,7 @@ fn debug_planes(
|
|||||||
(width, height): (u32, u32),
|
(width, height): (u32, u32),
|
||||||
) {
|
) {
|
||||||
let (depth, mut tupltype) = match colortype {
|
let (depth, mut tupltype) = match colortype {
|
||||||
|
// Note: we will expand, so this is in fact not BLACKANDWHITE
|
||||||
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
||||||
tiff::ColorType::RGB(_) => (3, "RGB"),
|
tiff::ColorType::RGB(_) => (3, "RGB"),
|
||||||
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
||||||
@@ -49,8 +50,57 @@ fn debug_planes(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Number of samples in a line and number of sample lines in the image, depending on whether
|
||||||
|
// they are planar or not. This determines the layout of the image buffer when expanding bit
|
||||||
|
// packed samples to a full byte (we support power-of-two sample sizes, positive and negative,
|
||||||
|
// but only those).
|
||||||
|
let (swidth, sheight) = if layout.planes > 1 {
|
||||||
|
(
|
||||||
|
u64::from(width) * u64::from(colortype.num_samples()),
|
||||||
|
u64::from(height),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
u64::from(width),
|
||||||
|
u64::from(height) * u64::from(colortype.num_samples()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fallback_buffer;
|
||||||
let maxval = match data.as_buffer(0) {
|
let maxval = match data.as_buffer(0) {
|
||||||
DecodingBuffer::U8(_) => u64::from(u8::MAX),
|
// `U8` is also our representation of bit-packed color. PAM uses the smallest number of
|
||||||
|
// *bytes* needed to represent a sample but we must also adhere to maxval. We could explode
|
||||||
|
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
|
||||||
|
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
|
||||||
|
// you want to do that in your real code, go ahead.
|
||||||
|
DecodingBuffer::U8(buf) => match colortype.bit_depth() {
|
||||||
|
8 => u64::from(u8::MAX),
|
||||||
|
4 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_4bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_2bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
fallback_buffer = vec![0; (swidth * sheight) as usize];
|
||||||
|
expand_1bit(buf, swidth, &mut fallback_buffer);
|
||||||
|
*data = DecodingResult::U8(fallback_buffer);
|
||||||
|
u64::from(u8::MAX)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!(
|
||||||
|
"Unsupported bit depth for U8 buffer: {}",
|
||||||
|
colortype.bit_depth()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
||||||
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
||||||
DecodingBuffer::U64(_) => u64::MAX,
|
DecodingBuffer::U64(_) => u64::MAX,
|
||||||
@@ -101,3 +151,56 @@ fn debug_planes(
|
|||||||
.expect("Failed to write PAM data");
|
.expect("Failed to write PAM data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put exactly the ith `SCALE`-bit chunk of `val` into the least significant bits.
|
||||||
|
#[inline(always)]
|
||||||
|
fn bits_extract<const SCALE: u8>(val: u8, i: u8) -> u8 {
|
||||||
|
// Shift left to discard high bits, then right to bring desired bits down. This way also
|
||||||
|
// conveniently avoids all cases of overflow.
|
||||||
|
(val << (i * SCALE)) >> (8 - SCALE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_1bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(8);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(8).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 8, _>(|i| 0xff * bits_extract::<1>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_2bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(4);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(4).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 4, _>(|i| 0xaa * bits_extract::<2>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_4bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
|
||||||
|
let stride = samples_per_padded.div_ceil(2);
|
||||||
|
|
||||||
|
let from_row = from.chunks_exact(stride as usize);
|
||||||
|
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
|
||||||
|
|
||||||
|
for (from_row, into_row) in from_row.zip(into_row) {
|
||||||
|
for (into, &from) in into_row.chunks_mut(2).zip(from_row) {
|
||||||
|
let data =
|
||||||
|
core::array::from_fn::<u8, 2, _>(|i| 0x88 * bits_extract::<4>(from, i as u8));
|
||||||
|
into.copy_from_slice(&data[..into.len()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use tiff::decoder::Decoder;
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
ls_dir(&mut reader);
|
||||||
|
|
||||||
|
if !reader.more_images() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
|
||||||
|
if let Some(ifd) = reader.ifd_pointer() {
|
||||||
|
println!("Directory at {ifd:x}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Name\tHex\tType\tCount");
|
||||||
|
let ifd = reader.image_ifd();
|
||||||
|
|
||||||
|
for (tag, entry) in ifd.directory().iter() {
|
||||||
|
let name: &'static str = match tag {
|
||||||
|
tiff::tags::Tag::Artist => "Artist",
|
||||||
|
tiff::tags::Tag::BitsPerSample => "BitsPerSample",
|
||||||
|
tiff::tags::Tag::CellLength => "CellLength",
|
||||||
|
tiff::tags::Tag::CellWidth => "CellWidth",
|
||||||
|
tiff::tags::Tag::ColorMap => "ColorMap",
|
||||||
|
tiff::tags::Tag::Compression => "Compression",
|
||||||
|
tiff::tags::Tag::DateTime => "DateTime",
|
||||||
|
tiff::tags::Tag::ExtraSamples => "ExtraSamples",
|
||||||
|
tiff::tags::Tag::FillOrder => "FillOrder",
|
||||||
|
tiff::tags::Tag::FreeByteCounts => "FreeByteCounts",
|
||||||
|
tiff::tags::Tag::FreeOffsets => "FreeOffsets",
|
||||||
|
tiff::tags::Tag::GrayResponseCurve => "GrayResponseCurve",
|
||||||
|
tiff::tags::Tag::GrayResponseUnit => "GrayResponseUnit",
|
||||||
|
tiff::tags::Tag::HostComputer => "HostComputer",
|
||||||
|
tiff::tags::Tag::ImageDescription => "ImageDescription",
|
||||||
|
tiff::tags::Tag::ImageLength => "ImageLength",
|
||||||
|
tiff::tags::Tag::ImageWidth => "ImageWidth",
|
||||||
|
tiff::tags::Tag::Make => "Make",
|
||||||
|
tiff::tags::Tag::MaxSampleValue => "MaxSampleValue",
|
||||||
|
tiff::tags::Tag::MinSampleValue => "MinSampleValue",
|
||||||
|
tiff::tags::Tag::Model => "Model",
|
||||||
|
tiff::tags::Tag::NewSubfileType => "NewSubfileType",
|
||||||
|
tiff::tags::Tag::Orientation => "Orientation",
|
||||||
|
tiff::tags::Tag::PhotometricInterpretation => "PhotometricInterpretation",
|
||||||
|
tiff::tags::Tag::PlanarConfiguration => "PlanarConfiguration",
|
||||||
|
tiff::tags::Tag::ResolutionUnit => "ResolutionUnit",
|
||||||
|
tiff::tags::Tag::RowsPerStrip => "RowsPerStrip",
|
||||||
|
tiff::tags::Tag::SamplesPerPixel => "SamplesPerPixel",
|
||||||
|
tiff::tags::Tag::Software => "Software",
|
||||||
|
tiff::tags::Tag::StripByteCounts => "StripByteCounts",
|
||||||
|
tiff::tags::Tag::StripOffsets => "StripOffsets",
|
||||||
|
tiff::tags::Tag::SubfileType => "SubfileType",
|
||||||
|
tiff::tags::Tag::Threshholding => "Threshholding",
|
||||||
|
tiff::tags::Tag::XResolution => "XResolution",
|
||||||
|
tiff::tags::Tag::YResolution => "YResolution",
|
||||||
|
tiff::tags::Tag::Predictor => "Predictor",
|
||||||
|
tiff::tags::Tag::TileWidth => "TileWidth",
|
||||||
|
tiff::tags::Tag::TileLength => "TileLength",
|
||||||
|
tiff::tags::Tag::TileOffsets => "TileOffsets",
|
||||||
|
tiff::tags::Tag::TileByteCounts => "TileByteCounts",
|
||||||
|
tiff::tags::Tag::SubIfd => "SubIfd",
|
||||||
|
tiff::tags::Tag::SampleFormat => "SampleFormat",
|
||||||
|
tiff::tags::Tag::SMinSampleValue => "SMinSampleValue",
|
||||||
|
tiff::tags::Tag::SMaxSampleValue => "SMaxSampleValue",
|
||||||
|
tiff::tags::Tag::JPEGTables => "JPEGTables",
|
||||||
|
tiff::tags::Tag::ChromaSubsampling => "ChromaSubsampling",
|
||||||
|
tiff::tags::Tag::ChromaPositioning => "ChromaPositioning",
|
||||||
|
tiff::tags::Tag::ModelPixelScaleTag => "ModelPixelScaleTag",
|
||||||
|
tiff::tags::Tag::ModelTransformationTag => "ModelTransformationTag",
|
||||||
|
tiff::tags::Tag::ModelTiepointTag => "ModelTiepointTag",
|
||||||
|
tiff::tags::Tag::Copyright => "Copyright",
|
||||||
|
tiff::tags::Tag::ExifDirectory => "ExifDirectory",
|
||||||
|
tiff::tags::Tag::GpsDirectory => "GpsDirectory",
|
||||||
|
tiff::tags::Tag::IccProfile => "IccProfile",
|
||||||
|
tiff::tags::Tag::GeoKeyDirectoryTag => "GeoKeyDirectoryTag",
|
||||||
|
tiff::tags::Tag::GeoDoubleParamsTag => "GeoDoubleParamsTag",
|
||||||
|
tiff::tags::Tag::GeoAsciiParamsTag => "GeoAsciiParamsTag",
|
||||||
|
tiff::tags::Tag::ExifVersion => "ExifVersion",
|
||||||
|
tiff::tags::Tag::GdalNodata => "GdalNodata",
|
||||||
|
_ => "<unknown>",
|
||||||
|
};
|
||||||
|
|
||||||
|
let ty: Cow<'static, str> = match entry.field_type() {
|
||||||
|
tiff::tags::Type::BYTE => "u8".into(),
|
||||||
|
tiff::tags::Type::ASCII => "ascii".into(),
|
||||||
|
tiff::tags::Type::SHORT => "u16".into(),
|
||||||
|
tiff::tags::Type::LONG => "u32".into(),
|
||||||
|
tiff::tags::Type::RATIONAL => "r32".into(),
|
||||||
|
tiff::tags::Type::SBYTE => "i8".into(),
|
||||||
|
tiff::tags::Type::UNDEFINED => "byte".into(),
|
||||||
|
tiff::tags::Type::SSHORT => "s16".into(),
|
||||||
|
tiff::tags::Type::SLONG => "s32".into(),
|
||||||
|
tiff::tags::Type::SRATIONAL => "sr32".into(),
|
||||||
|
tiff::tags::Type::FLOAT => "f32".into(),
|
||||||
|
tiff::tags::Type::DOUBLE => "f64".into(),
|
||||||
|
tiff::tags::Type::IFD => "ifd32".into(),
|
||||||
|
tiff::tags::Type::LONG8 => "u64".into(),
|
||||||
|
tiff::tags::Type::SLONG8 => "i64".into(),
|
||||||
|
tiff::tags::Type::IFD8 => "ifd64".into(),
|
||||||
|
other => format!("{:x}", other.to_u16()).into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{name:16}\t{tag:4x}\t{ty}\t{count}",
|
||||||
|
tag = tag.to_u16(),
|
||||||
|
count = entry.count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+144
-12
@@ -6,12 +6,12 @@ use std::mem;
|
|||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
use super::stream::{ByteOrder, EndianReader};
|
use super::stream::{ByteOrder, EndianReader};
|
||||||
use crate::tags::{IfdPointer, Tag, Type};
|
use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
|
||||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||||
|
|
||||||
use self::Value::{
|
use self::Value::{
|
||||||
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
|
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
|
||||||
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
|
SignedByte, SignedShort, Unsigned, UnsignedBig,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(unused_qualifications)]
|
#[allow(unused_qualifications)]
|
||||||
@@ -30,8 +30,16 @@ pub enum Value {
|
|||||||
Double(f64),
|
Double(f64),
|
||||||
List(Vec<Value>),
|
List(Vec<Value>),
|
||||||
Rational(u32, u32),
|
Rational(u32, u32),
|
||||||
|
#[deprecated(
|
||||||
|
note = "Not implemented in BigTIFF with a standard tag value",
|
||||||
|
since = "0.11.1"
|
||||||
|
)]
|
||||||
RationalBig(u64, u64),
|
RationalBig(u64, u64),
|
||||||
SRational(i32, i32),
|
SRational(i32, i32),
|
||||||
|
#[deprecated(
|
||||||
|
note = "Not implemented in BigTIFF with a standard tag value",
|
||||||
|
since = "0.11.1"
|
||||||
|
)]
|
||||||
SRationalBig(i64, i64),
|
SRationalBig(i64, i64),
|
||||||
Ascii(String),
|
Ascii(String),
|
||||||
Ifd(u32),
|
Ifd(u32),
|
||||||
@@ -170,7 +178,8 @@ impl Value {
|
|||||||
Unsigned(val) => Ok(vec![val]),
|
Unsigned(val) => Ok(vec![val]),
|
||||||
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
|
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
|
||||||
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
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)?])
|
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
|
||||||
}
|
}
|
||||||
Ifd(val) => Ok(vec![val]),
|
Ifd(val) => Ok(vec![val]),
|
||||||
@@ -220,7 +229,8 @@ impl Value {
|
|||||||
new_vec.push(numerator);
|
new_vec.push(numerator);
|
||||||
new_vec.push(denominator);
|
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(numerator)?);
|
||||||
new_vec.push(i32::try_from(denominator)?);
|
new_vec.push(i32::try_from(denominator)?);
|
||||||
}
|
}
|
||||||
@@ -234,7 +244,8 @@ impl Value {
|
|||||||
Signed(val) => Ok(vec![val]),
|
Signed(val) => Ok(vec![val]),
|
||||||
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
|
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
|
||||||
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
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)?])
|
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
|
||||||
}
|
}
|
||||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||||
@@ -283,7 +294,8 @@ impl Value {
|
|||||||
Unsigned(val) => Ok(vec![val.into()]),
|
Unsigned(val) => Ok(vec![val.into()]),
|
||||||
UnsignedBig(val) => Ok(vec![val]),
|
UnsignedBig(val) => Ok(vec![val]),
|
||||||
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
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()]),
|
Ifd(val) => Ok(vec![val.into()]),
|
||||||
IfdBig(val) => Ok(vec![val]),
|
IfdBig(val) => Ok(vec![val]),
|
||||||
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
|
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(numerator.into());
|
||||||
new_vec.push(denominator.into());
|
new_vec.push(denominator.into());
|
||||||
}
|
}
|
||||||
SRationalBig(numerator, denominator) => {
|
#[expect(deprecated)]
|
||||||
|
Value::SRationalBig(numerator, denominator) => {
|
||||||
new_vec.push(numerator);
|
new_vec.push(numerator);
|
||||||
new_vec.push(denominator);
|
new_vec.push(denominator);
|
||||||
}
|
}
|
||||||
@@ -315,7 +328,8 @@ impl Value {
|
|||||||
Signed(val) => Ok(vec![val.into()]),
|
Signed(val) => Ok(vec![val.into()]),
|
||||||
SignedBig(val) => Ok(vec![val]),
|
SignedBig(val) => Ok(vec![val]),
|
||||||
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
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)),
|
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,7 +579,7 @@ impl Entry {
|
|||||||
|
|
||||||
// Case 4: there is more than one value, and it doesn't fit in the offset field.
|
// Case 4: there is more than one value, and it doesn't fit in the offset field.
|
||||||
let mut v;
|
let mut v;
|
||||||
self.set_reader_offset(bo, bigtiff, reader)?;
|
self.set_reader_offset_relative(bigtiff, reader, 0)?;
|
||||||
|
|
||||||
match self.type_ {
|
match self.type_ {
|
||||||
Type::BYTE | Type::UNDEFINED => {
|
Type::BYTE | Type::UNDEFINED => {
|
||||||
@@ -723,6 +737,113 @@ impl Entry {
|
|||||||
Ok(List(v))
|
Ok(List(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn buffered_value<R: Read + Seek>(
|
||||||
|
&self,
|
||||||
|
buf: &mut ValueBuffer,
|
||||||
|
limits: &super::Limits,
|
||||||
|
bigtiff: bool,
|
||||||
|
reader: &mut EndianReader<R>,
|
||||||
|
) -> TiffResult<()> {
|
||||||
|
if self.count == 0 {
|
||||||
|
buf.assume_type(self.type_, 0, reader.byte_order);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let value_bytes = self.buffer_with_capacity(buf, limits)?;
|
||||||
|
|
||||||
|
// Case 1: the value fits in the offset field.
|
||||||
|
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
|
||||||
|
let src = &self.offset[..value_bytes];
|
||||||
|
buf.raw_bytes_mut()[..value_bytes].copy_from_slice(src);
|
||||||
|
buf.assume_type(self.type_, self.count, reader.byte_order);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: the value is stored in the reader at an offset.
|
||||||
|
self.set_reader_offset_relative(bigtiff, reader, 0)?;
|
||||||
|
|
||||||
|
// In case of an error we set the type and endianess.
|
||||||
|
buf.assume_type(self.type_, 0, reader.byte_order);
|
||||||
|
let target = &mut buf.raw_bytes_mut()[..value_bytes];
|
||||||
|
// FIXME: if the read fails we have already grown to full size, which is not great.
|
||||||
|
reader.inner().read_exact(target)?;
|
||||||
|
buf.assume_type(self.type_, self.count, reader.byte_order);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raw_value_at<R: Read + Seek>(
|
||||||
|
&self,
|
||||||
|
buf: &mut [u8],
|
||||||
|
bigtiff: bool,
|
||||||
|
reader: &mut EndianReader<R>,
|
||||||
|
at: u64,
|
||||||
|
) -> TiffResult<usize> {
|
||||||
|
if self.count == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have no limits to handle, we do not allocate.
|
||||||
|
let value_bytes = self.type_.value_bytes(self.count)?;
|
||||||
|
|
||||||
|
// No bytes to fill into the buffer.
|
||||||
|
if at >= value_bytes {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 1: the value fits in the offset field.
|
||||||
|
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
|
||||||
|
// `at < value_bytes` and `value_bytes <= 8` so casting is mathematical
|
||||||
|
let src = &self.offset[..value_bytes as usize][at as usize..];
|
||||||
|
let len = src.len().min(buf.len());
|
||||||
|
buf[..len].copy_from_slice(&src[..len]);
|
||||||
|
return Ok(value_bytes as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: the value is stored in the reader at an offset. We will find the offset
|
||||||
|
// encoded in the entry, apply the relative start position and seek there.
|
||||||
|
self.set_reader_offset_relative(bigtiff, reader, at)?;
|
||||||
|
|
||||||
|
let remainder = value_bytes - at;
|
||||||
|
let len = usize::try_from(remainder)
|
||||||
|
.unwrap_or(usize::MAX)
|
||||||
|
.min(buf.len());
|
||||||
|
|
||||||
|
let target = &mut buf[..len];
|
||||||
|
reader.inner().read_exact(target)?;
|
||||||
|
|
||||||
|
// Design note: in a previous draft we would consume the rest of the bytes of this value
|
||||||
|
// here (into a stack buffer if need be) to verify the stream itself. But in the end we
|
||||||
|
// have `Seek` so we better verify this by seeking over the rest of the bytes, finding if
|
||||||
|
// the stream continues that far. Even that is maybe bad if we wanted to provide a
|
||||||
|
// async-adaptor that `WouldBlock` errors to fill back a read window then the seek is
|
||||||
|
// poison to that, too.
|
||||||
|
|
||||||
|
// So a really simple choice: The caller is responsible for handling the fact that this did
|
||||||
|
// not verify the whole value. Attempt a 1-byte read at the end of the value instead?
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns `Ok(bytes)` if our value's bytes through type and count fit into `usize` and are
|
||||||
|
// within the limits. Extends the buffer to that many bytes.
|
||||||
|
fn buffer_with_capacity(
|
||||||
|
&self,
|
||||||
|
buf: &mut ValueBuffer,
|
||||||
|
limits: &super::Limits,
|
||||||
|
) -> TiffResult<usize> {
|
||||||
|
let bytes = self.type_.value_bytes(self.count())?;
|
||||||
|
|
||||||
|
let allowed_length = usize::try_from(bytes)
|
||||||
|
.ok()
|
||||||
|
.filter(|&n| n <= limits.decoding_buffer_size)
|
||||||
|
.ok_or(TiffError::LimitsExceeded)?;
|
||||||
|
|
||||||
|
buf.prepare_length(allowed_length);
|
||||||
|
|
||||||
|
Ok(allowed_length)
|
||||||
|
}
|
||||||
|
|
||||||
fn vec_with_capacity(
|
fn vec_with_capacity(
|
||||||
value_count: u64,
|
value_count: u64,
|
||||||
limits: &super::Limits,
|
limits: &super::Limits,
|
||||||
@@ -736,21 +857,32 @@ impl Entry {
|
|||||||
Ok(Vec::with_capacity(value_count))
|
Ok(Vec::with_capacity(value_count))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_reader_offset<R>(
|
/// Seek to an offset within a value stored in the offset defined by this entry.
|
||||||
|
fn set_reader_offset_relative<R>(
|
||||||
&self,
|
&self,
|
||||||
bo: ByteOrder,
|
|
||||||
bigtiff: bool,
|
bigtiff: bool,
|
||||||
reader: &mut EndianReader<R>,
|
reader: &mut EndianReader<R>,
|
||||||
|
at: u64,
|
||||||
) -> TiffResult<()>
|
) -> TiffResult<()>
|
||||||
where
|
where
|
||||||
R: Read + Seek,
|
R: Read + Seek,
|
||||||
{
|
{
|
||||||
|
let bo = reader.byte_order;
|
||||||
|
|
||||||
let offset = if bigtiff {
|
let offset = if bigtiff {
|
||||||
self.offset_field_reader(bo).read_u64()?
|
self.offset_field_reader(bo).read_u64()?
|
||||||
} else {
|
} else {
|
||||||
self.offset_field_reader(bo).read_u32()?.into()
|
self.offset_field_reader(bo).read_u32()?.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FIXME: `at` should be within `self.type_.value_bytes(self.count)` and that itself should
|
||||||
|
// be within the bounds of the stream. But we do not check this eagerly so this below will
|
||||||
|
// fail sometimes differently for exotic streams, depending on the method by which we read
|
||||||
|
// (at once or through multiple raw into-byte-slice reads).
|
||||||
|
let offset = offset.checked_add(at).ok_or(TiffError::FormatError(
|
||||||
|
TiffFormatError::InconsistentSizesEncountered,
|
||||||
|
))?;
|
||||||
|
|
||||||
reader.goto_offset(offset)?;
|
reader.goto_offset(offset)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+111
-6
@@ -85,6 +85,7 @@ pub(crate) struct Image {
|
|||||||
pub tile_attributes: Option<TileAttributes>,
|
pub tile_attributes: Option<TileAttributes>,
|
||||||
pub chunk_offsets: Vec<u64>,
|
pub chunk_offsets: Vec<u64>,
|
||||||
pub chunk_bytes: Vec<u64>,
|
pub chunk_bytes: Vec<u64>,
|
||||||
|
pub chroma_subsampling: (u16, u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes how to read a tile-aligned portion of the image.
|
/// Describes how to read a tile-aligned portion of the image.
|
||||||
@@ -95,6 +96,19 @@ pub(crate) struct ReadoutLayout {
|
|||||||
pub planar_config: PlanarConfiguration,
|
pub planar_config: PlanarConfiguration,
|
||||||
|
|
||||||
/// The sample interpretation (interpret with planar_config).
|
/// 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,
|
pub color: ColorType,
|
||||||
/// The number of bytes from one row to another.
|
/// The number of bytes from one row to another.
|
||||||
pub minimum_row_stride: usize,
|
pub minimum_row_stride: usize,
|
||||||
@@ -262,6 +276,25 @@ impl Image {
|
|||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or(PlanarConfiguration::Chunky);
|
.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 {
|
let planes = match planar_config {
|
||||||
PlanarConfiguration::Chunky => 1,
|
PlanarConfiguration::Chunky => 1,
|
||||||
PlanarConfiguration::Planar => samples,
|
PlanarConfiguration::Planar => samples,
|
||||||
@@ -373,6 +406,7 @@ impl Image {
|
|||||||
tile_attributes,
|
tile_attributes,
|
||||||
chunk_offsets,
|
chunk_offsets,
|
||||||
chunk_bytes,
|
chunk_bytes,
|
||||||
|
chroma_subsampling,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,25 +469,71 @@ impl Image {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: this is bad we should not fail at this point
|
// ```
|
||||||
PhotometricInterpretation::RGBPalette
|
// struct IccLab /* Interpretation 9* {
|
||||||
| PhotometricInterpretation::TransparencyMask
|
// pub L: u8, // SampleFormat::Uint
|
||||||
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
// 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(
|
TiffUnsupportedError::InterpretationWithBits(
|
||||||
self.photometric_interpretation,
|
self.photometric_interpretation,
|
||||||
vec![self.bits_per_sample; self.samples as usize],
|
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(
|
||||||
|
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],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_reader<'r, R: 'r + Read>(
|
fn create_reader<'r, R: 'r + Read + Seek>(
|
||||||
reader: R,
|
reader: R,
|
||||||
compression_method: CompressionMethod,
|
compression_method: CompressionMethod,
|
||||||
compressed_length: u64,
|
compressed_length: u64,
|
||||||
// FIXME: these should be `expect` attributes or we choose another way of passing them.
|
// FIXME: these should be `expect` attributes or we choose another way of passing them.
|
||||||
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
|
||||||
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
|
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
|
||||||
|
#[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16,
|
||||||
) -> TiffResult<Box<dyn Read + 'r>> {
|
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||||
Ok(match compression_method {
|
Ok(match compression_method {
|
||||||
CompressionMethod::None => Box::new(reader),
|
CompressionMethod::None => Box::new(reader),
|
||||||
@@ -528,6 +608,13 @@ impl Image {
|
|||||||
reader,
|
reader,
|
||||||
compressed_length,
|
compressed_length,
|
||||||
)?),
|
)?),
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
|
||||||
|
reader,
|
||||||
|
compressed_length,
|
||||||
|
samples,
|
||||||
|
)?),
|
||||||
|
|
||||||
method => {
|
method => {
|
||||||
return Err(TiffError::UnsupportedError(
|
return Err(TiffError::UnsupportedError(
|
||||||
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
||||||
@@ -657,6 +744,23 @@ impl Image {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
Ok(ReadoutLayout {
|
||||||
planar_config: self.planar_config,
|
planar_config: self.planar_config,
|
||||||
color,
|
color,
|
||||||
@@ -709,7 +813,7 @@ impl Image {
|
|||||||
|
|
||||||
pub(crate) fn expand_chunk(
|
pub(crate) fn expand_chunk(
|
||||||
&self,
|
&self,
|
||||||
reader: &mut ValueReader<impl Read>,
|
reader: &mut ValueReader<impl Read + Seek>,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
layout: &ReadoutLayout,
|
layout: &ReadoutLayout,
|
||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
@@ -823,6 +927,7 @@ impl Image {
|
|||||||
*compressed_bytes,
|
*compressed_bytes,
|
||||||
self.jpeg_tables.as_deref().map(|a| &**a),
|
self.jpeg_tables.as_deref().map(|a| &**a),
|
||||||
chunk_dims,
|
chunk_dims,
|
||||||
|
self.samples,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if is_output_chunk_rows && is_all_bits {
|
if is_output_chunk_rows && is_all_bits {
|
||||||
|
|||||||
+147
-11
@@ -5,7 +5,7 @@ use std::num::NonZeroUsize;
|
|||||||
|
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||||
SampleFormat, Tag, Type,
|
SampleFormat, Tag, Type, ValueBuffer,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
|
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
|
||||||
@@ -860,6 +860,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
tile_attributes: None,
|
tile_attributes: None,
|
||||||
chunk_offsets: Vec::new(),
|
chunk_offsets: Vec::new(),
|
||||||
chunk_bytes: Vec::new(),
|
chunk_bytes: Vec::new(),
|
||||||
|
chroma_subsampling: (2, 2),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
decoder.next_image()?;
|
decoder.next_image()?;
|
||||||
@@ -1277,7 +1278,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
///
|
///
|
||||||
/// It will re-allocate the buffer into the correct type and size, within the decoder's
|
/// 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
|
/// configured limits, and then pass it to the underlying method. This is essentially a
|
||||||
/// type-safe wrapper around the raw [`Self::read_chunk_to_bytes`] method.
|
/// 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.
|
/// Note that for planar images each chunk contains only one sample of the underlying data.
|
||||||
pub fn read_chunk_to_buffer(
|
pub fn read_chunk_to_buffer(
|
||||||
@@ -1395,14 +1396,16 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// # Bugs
|
/// # Bugs
|
||||||
///
|
///
|
||||||
/// When the image is stored as a planar configuration, this method will currently only read
|
/// 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`.
|
/// the first sample's plane. This will be fixed in a future major version of `tiff`. To read
|
||||||
|
/// multiple planes, [`Self::read_image_to_buffer`] can be used instead.
|
||||||
///
|
///
|
||||||
/// # Intent to deprecate
|
/// # Intent to deprecate
|
||||||
///
|
///
|
||||||
/// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving
|
/// Use [`Self::read_image_to_buffer`] or a combination of [`DecodingResult::resize_to`] and
|
||||||
/// the buffer across multiple calls. This old method will likely keep its bugged planar
|
/// [`Self::read_image_bytes`] instead where possible, preserving the buffer across multiple
|
||||||
/// behavior until it is fully replaced, to ensure that existing code will not run into
|
/// calls. This old method will likely keep its bugged planar behavior until it is fully
|
||||||
/// unexpectedly large allocations that will error on limits instead.
|
/// 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> {
|
pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
|
||||||
let readout = self.image().readout_for_image()?;
|
let readout = self.image().readout_for_image()?;
|
||||||
|
|
||||||
@@ -1421,6 +1424,30 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
/// configured limits, and then pass it to the underlying method. This is essentially a
|
/// 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.
|
/// type-safe wrapper around the raw [`Self::read_image_bytes`] method.
|
||||||
///
|
///
|
||||||
|
/// ## Planar behavior
|
||||||
|
///
|
||||||
|
/// If the image is stored as a planar configuration, an attempt is made to resize the buffer
|
||||||
|
/// to hold all planes. If that does not fit then only the first plane is read. Check the
|
||||||
|
/// buffer size against [`BufferLayoutPreference::complete_len`] to ensure that all planes were
|
||||||
|
/// read:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tiff::decoder::{Decoder, DecodingResult};
|
||||||
|
/// let mut result = DecodingResult::U8(vec![]);
|
||||||
|
///
|
||||||
|
/// let mut reader = /* */
|
||||||
|
/// # Decoder::new(std::io::Cursor::new(include_bytes!(concat!(
|
||||||
|
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
|
||||||
|
/// # )))).unwrap();
|
||||||
|
/// let layout = reader.read_image_to_buffer(&mut result)?;
|
||||||
|
///
|
||||||
|
/// if result.as_buffer(0).as_bytes().len() < layout.complete_len {
|
||||||
|
/// println!("Only the first plane was read");
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// # Ok::<_, tiff::TiffError>(())
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -1442,12 +1469,20 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
result: &mut DecodingResult,
|
result: &mut DecodingResult,
|
||||||
) -> TiffResult<BufferLayoutPreference> {
|
) -> TiffResult<BufferLayoutPreference> {
|
||||||
let readout = self.image().readout_for_image()?;
|
let readout = self.image().readout_for_image()?;
|
||||||
|
|
||||||
let planes = readout.to_plane_layout()?;
|
let planes = readout.to_plane_layout()?;
|
||||||
let layout = BufferLayoutPreference::from_planes(&planes);
|
|
||||||
|
|
||||||
let extent = readout.result_extent_for_planes(0..1)?;
|
let num_planes = if planes.total_bytes <= self.value_reader.limits.decoding_buffer_size {
|
||||||
|
planes.plane_offsets.len() as u16
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = BufferLayoutPreference::from_planes(&planes);
|
||||||
|
let extent = readout.result_extent_for_planes(0..num_planes)?;
|
||||||
|
// Compatibility: if this extent is too large our configured limits we will fall back to
|
||||||
|
// reading only the first plane.
|
||||||
result.resize_to_extent(extent, &self.value_reader.limits)?;
|
result.resize_to_extent(extent, &self.value_reader.limits)?;
|
||||||
|
|
||||||
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
|
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
|
||||||
|
|
||||||
Ok(layout)
|
Ok(layout)
|
||||||
@@ -1503,7 +1538,7 @@ impl<R: Read + Seek> Decoder<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the IFD decoder for our current image IFD.
|
/// Get the IFD decoder for our current image IFD.
|
||||||
fn image_ifd(&mut self) -> IfdDecoder<'_> {
|
pub fn image_ifd(&mut self) -> IfdDecoder<'_> {
|
||||||
IfdDecoder {
|
IfdDecoder {
|
||||||
inner: tag_reader::TagReader {
|
inner: tag_reader::TagReader {
|
||||||
decoder: &mut self.value_reader,
|
decoder: &mut self.value_reader,
|
||||||
@@ -1716,12 +1751,41 @@ impl<R: Seek + Read> ValueReader<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IfdDecoder<'_> {
|
impl IfdDecoder<'_> {
|
||||||
|
/// Retrieve the IFD entry for a given tag, if it exists.
|
||||||
|
///
|
||||||
|
/// The entry contains the metadata of the value, that is its type and count from which we can
|
||||||
|
/// calculate a total byte size.
|
||||||
|
pub fn find_entry(&self, tag: Tag) -> Option<ifd::Entry> {
|
||||||
|
self.inner.ifd.get(tag).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Tries to retrieve a tag.
|
/// Tries to retrieve a tag.
|
||||||
/// Return `Ok(None)` if the tag is not present.
|
/// Return `Ok(None)` if the tag is not present.
|
||||||
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
|
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
|
||||||
self.inner.find_tag(tag)
|
self.inner.find_tag(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve a tag and reproduce its bytes into the provided buffer.
|
||||||
|
///
|
||||||
|
/// The buffer is unmodified if the tag is not present.
|
||||||
|
pub fn find_tag_buf(
|
||||||
|
&mut self,
|
||||||
|
tag: Tag,
|
||||||
|
buf: &mut ValueBuffer,
|
||||||
|
) -> TiffResult<Option<ifd::Entry>> {
|
||||||
|
self.inner.find_tag_buf(tag, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read bytes of a tag's value into a byte buffer.
|
||||||
|
pub fn find_tag_bytes(
|
||||||
|
&mut self,
|
||||||
|
tag: Tag,
|
||||||
|
buf: &mut [u8],
|
||||||
|
offset: u64,
|
||||||
|
) -> TiffResult<Option<usize>> {
|
||||||
|
self.inner.find_tag_raw(tag, buf, offset)
|
||||||
|
}
|
||||||
|
|
||||||
/// Tries to retrieve a tag and convert it to the desired unsigned type.
|
/// Tries to retrieve a tag and convert it to the desired unsigned type.
|
||||||
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
|
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
|
||||||
self.find_tag(tag)?
|
self.find_tag(tag)?
|
||||||
@@ -1821,6 +1885,11 @@ impl IfdDecoder<'_> {
|
|||||||
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
|
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
|
||||||
self.get_tag(tag)?.into_string()
|
self.get_tag(tag)?.into_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspect the raw underlying directory.
|
||||||
|
pub fn directory(&self) -> &Directory {
|
||||||
|
self.inner.ifd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> IfdDecoder<'l> {
|
impl<'l> IfdDecoder<'l> {
|
||||||
@@ -1835,3 +1904,70 @@ impl<'l> IfdDecoder<'l> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Decoder;
|
||||||
|
use crate::{
|
||||||
|
bytecast,
|
||||||
|
tags::{ByteOrder, Tag, ValueBuffer},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equivalence_of_tag_readers() {
|
||||||
|
let file = std::fs::File::open(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/tests/images/int8_rgb.tif"
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut decoder = Decoder::new(file).unwrap();
|
||||||
|
let file_bo = decoder.byte_order();
|
||||||
|
let mut ifd = decoder.image_ifd();
|
||||||
|
|
||||||
|
{
|
||||||
|
let value = ifd
|
||||||
|
.find_tag(Tag::BitsPerSample)
|
||||||
|
.unwrap()
|
||||||
|
.expect("must have BitsPerSample");
|
||||||
|
|
||||||
|
let samples = value.into_u16_vec().unwrap();
|
||||||
|
assert_eq!(samples.as_slice(), [8, 8, 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut value = ValueBuffer::from_value(&[0u16; 4]);
|
||||||
|
let _entry = ifd
|
||||||
|
.find_tag_buf(Tag::BitsPerSample, &mut value)
|
||||||
|
.unwrap()
|
||||||
|
.expect("must have BitsPerSample");
|
||||||
|
|
||||||
|
value.set_byte_order(ByteOrder::native());
|
||||||
|
assert_eq!(value.as_bytes(), bytecast::u16_as_ne_bytes(&[8, 8, 8]));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut by_bytes = [0u16; 4];
|
||||||
|
let entry = ifd
|
||||||
|
.find_entry(Tag::BitsPerSample)
|
||||||
|
.expect("must have BitsPerSample");
|
||||||
|
|
||||||
|
let byte_len = ifd
|
||||||
|
.find_tag_bytes(
|
||||||
|
Tag::BitsPerSample,
|
||||||
|
bytecast::u16_as_ne_mut_bytes(&mut by_bytes),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.expect("must have BitsPerSample");
|
||||||
|
assert_eq!(byte_len, 3 * std::mem::size_of::<u16>());
|
||||||
|
|
||||||
|
file_bo.convert(
|
||||||
|
entry.field_type(),
|
||||||
|
bytecast::u16_as_ne_mut_bytes(&mut by_bytes[..3]),
|
||||||
|
ByteOrder::native(),
|
||||||
|
);
|
||||||
|
assert_eq!(&by_bytes[..3], &[8, 8, 8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
//! All IO functionality needed for TIFF decoding
|
//! All IO functionality needed for TIFF decoding
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
use std::io::Cursor;
|
||||||
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
||||||
|
|
||||||
pub use crate::tags::ByteOrder;
|
pub use crate::tags::ByteOrder;
|
||||||
@@ -354,6 +356,61 @@ impl<R: Read> Read for Group4Reader<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
pub struct WebPReader {
|
||||||
|
inner: Cursor<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
impl WebPReader {
|
||||||
|
pub fn new<R: Read + Seek>(
|
||||||
|
reader: R,
|
||||||
|
compressed_length: u64,
|
||||||
|
samples: u16,
|
||||||
|
) -> crate::TiffResult<Self> {
|
||||||
|
let mut decoder =
|
||||||
|
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
|
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"bad sample count for WebP compressed data",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_bytes =
|
||||||
|
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
|
||||||
|
let mut data = vec![0; total_bytes];
|
||||||
|
|
||||||
|
decoder
|
||||||
|
.read_image(&mut data)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
|
// Add a fully opaque alpha channel if needed
|
||||||
|
if samples == 4 && !decoder.has_alpha() {
|
||||||
|
for i in (0..(total_bytes / 4)).rev() {
|
||||||
|
data[i * 4 + 3] = 255;
|
||||||
|
data[i * 4 + 2] = data[i * 3 + 2];
|
||||||
|
data[i * 4 + 1] = data[i * 3 + 1];
|
||||||
|
data[i * 4] = data[i * 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
inner: Cursor::new(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
impl Read for WebPReader {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.inner.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::io::{Read, Seek};
|
use std::io::{Read, Seek};
|
||||||
|
|
||||||
use crate::{tags::Tag, Directory};
|
use crate::{
|
||||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
tags::{Tag, ValueBuffer},
|
||||||
|
Directory, TiffError, TiffFormatError, TiffResult,
|
||||||
|
};
|
||||||
|
|
||||||
use super::ifd::{Entry, Value};
|
use super::ifd::{Entry, Value};
|
||||||
use super::ValueReader;
|
use super::ValueReader;
|
||||||
@@ -14,12 +16,25 @@ pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
|
|||||||
pub(crate) trait EntryDecoder {
|
pub(crate) trait EntryDecoder {
|
||||||
/// Turn an entry into a value by fetching the required bytes in the stream.
|
/// Turn an entry into a value by fetching the required bytes in the stream.
|
||||||
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
|
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
|
||||||
|
/// Fill a `ValueBuffer` by fetching the required bytes.
|
||||||
|
fn entry_buf(&mut self, entry: &Entry, _: &mut ValueBuffer) -> TiffResult<()>;
|
||||||
|
/// Fill the prefix of the buffer and return the actual bytes. Start interpreting the inner
|
||||||
|
/// value at `offset`.
|
||||||
|
fn entry_raw(&mut self, entry: &Entry, _: &mut [u8], offset: u64) -> TiffResult<usize>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
|
impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
|
||||||
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
|
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
|
||||||
entry.val(&self.limits, self.bigtiff, &mut self.reader)
|
entry.val(&self.limits, self.bigtiff, &mut self.reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn entry_buf(&mut self, entry: &Entry, buf: &mut ValueBuffer) -> TiffResult<()> {
|
||||||
|
entry.buffered_value(buf, &self.limits, self.bigtiff, &mut self.reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_raw(&mut self, entry: &Entry, buf: &mut [u8], offset: u64) -> TiffResult<usize> {
|
||||||
|
entry.raw_value_at(buf, self.bigtiff, &mut self.reader, offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
|
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
|
||||||
@@ -32,6 +47,39 @@ impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
|
|||||||
self.decoder.entry_val(&entry).map(Some)
|
self.decoder.entry_val(&entry).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find a tag and fill the provided buffer with its raw data.
|
||||||
|
///
|
||||||
|
/// Returns the entry of the tag if found. This signals that the buffer was overwritten.
|
||||||
|
/// Otherwise the buffer is preserved as-is.
|
||||||
|
pub(crate) fn find_tag_buf(
|
||||||
|
&mut self,
|
||||||
|
tag: Tag,
|
||||||
|
buf: &mut ValueBuffer,
|
||||||
|
) -> TiffResult<Option<Entry>> {
|
||||||
|
let entry = match self.ifd.get(tag) {
|
||||||
|
None => return Ok(None),
|
||||||
|
Some(entry) => entry.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.decoder.entry_buf(&entry, buf)?;
|
||||||
|
Ok(Some(entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_tag_raw(
|
||||||
|
&mut self,
|
||||||
|
tag: Tag,
|
||||||
|
buf: &mut [u8],
|
||||||
|
at: u64,
|
||||||
|
) -> TiffResult<Option<usize>> {
|
||||||
|
let entry = match self.ifd.get(tag) {
|
||||||
|
None => return Ok(None),
|
||||||
|
Some(entry) => entry.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let len = self.decoder.entry_raw(&entry, buf, at)?;
|
||||||
|
Ok(Some(len))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
|
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
|
||||||
match self.find_tag(tag)? {
|
match self.find_tag(tag)? {
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
|
|||||||
+135
-13
@@ -11,7 +11,10 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
decoder::ifd::Entry,
|
decoder::ifd::Entry,
|
||||||
error::{TiffResult, UsageError},
|
error::{TiffResult, UsageError},
|
||||||
tags::{CompressionMethod, ExtraSamples, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
|
tags::{
|
||||||
|
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||||
|
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||||
|
},
|
||||||
Directory, TiffError, TiffFormatError,
|
Directory, TiffError, TiffFormatError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,12 +301,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
value.write(&mut writer)?;
|
value.write(&mut writer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = Self::write_value(
|
let entry = Self::write_entry_inner(
|
||||||
self.writer,
|
self.writer,
|
||||||
&DirectoryEntry {
|
DirectoryEntry {
|
||||||
data_type: <T>::FIELD_TYPE,
|
data_type: <T>::FIELD_TYPE,
|
||||||
count: value.count().try_into()?,
|
count: value.count().try_into()?,
|
||||||
data: bytes,
|
data: bytes.into(),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -312,15 +315,114 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a tag-value pair with prepared byte data.
|
||||||
|
///
|
||||||
|
/// Note that the library will _not_ attempt to verify that the data type or the count of the
|
||||||
|
/// buffered value is permissible for the given tag. The data will be associated with the tag
|
||||||
|
/// exactly as-is.
|
||||||
|
///
|
||||||
|
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||||
|
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||||
|
pub fn write_tag_buf(&mut self, tag: Tag, value: &ValueBuffer) -> TiffResult<()> {
|
||||||
|
let entry = self.write_entry_buf(value)?;
|
||||||
|
self.directory.extend([(tag, entry)]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Write some data to the tiff file, the offset of the data is returned.
|
/// Write some data to the tiff file, the offset of the data is returned.
|
||||||
///
|
///
|
||||||
/// This could be used to write tiff strips.
|
/// This could be used to write tiff strips. All of the data will be written into the file as a
|
||||||
|
/// slice of bytes. It can not be used as an entry in a directory directly, where very short
|
||||||
|
/// values are instead represented in the offset field directly. Use [`Self::write_entry`]
|
||||||
|
/// instead.
|
||||||
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
|
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
|
||||||
let offset = self.writer.offset();
|
let offset = self.writer.offset();
|
||||||
value.write(self.writer)?;
|
value.write(self.writer)?;
|
||||||
Ok(offset)
|
Ok(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write some data directly to the tiff file, the offset of the data is returned.
|
||||||
|
///
|
||||||
|
/// All of the data will be written into the file as a slice of bytes. It can not be used as an
|
||||||
|
/// entry in a directory directly, where very short values are instead represented in the
|
||||||
|
/// offset field directly. Use [`Self::write_entry_buf`] instead.
|
||||||
|
///
|
||||||
|
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||||
|
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||||
|
pub fn write_data_buf(&mut self, value: &ValueBuffer) -> TiffResult<u64> {
|
||||||
|
self.check_value_byteorder(value)?;
|
||||||
|
let offset = self.writer.offset();
|
||||||
|
self.writer.write_bytes(value.as_bytes())?;
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a directory value into the file.
|
||||||
|
///
|
||||||
|
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||||
|
/// stored in the entry's offset field.
|
||||||
|
pub fn write_entry<T: TiffValue>(&mut self, value: T) -> TiffResult<Entry> {
|
||||||
|
Self::write_entry_inner(
|
||||||
|
self.writer,
|
||||||
|
DirectoryEntry {
|
||||||
|
data_type: T::FIELD_TYPE,
|
||||||
|
count: K::convert_offset(value.count() as u64)?,
|
||||||
|
// FIXME: not optimal we always allocate here. But the only other method we have
|
||||||
|
// available is `T::write(&mut TiffWriter<W>)` and we must pass that into
|
||||||
|
// `write_entry_inner` but then have a combinatorial explosion of instantiations
|
||||||
|
// which is absurd.
|
||||||
|
data: value.data(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a directory value into the file.
|
||||||
|
///
|
||||||
|
/// An error is returned if the byte order of the presented value does not match the byte order
|
||||||
|
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
|
||||||
|
///
|
||||||
|
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||||
|
/// stored in the entry's offset field.
|
||||||
|
pub fn write_entry_buf(&mut self, value: &ValueBuffer) -> TiffResult<Entry> {
|
||||||
|
self.check_value_byteorder(value)?;
|
||||||
|
|
||||||
|
Self::write_entry_inner(
|
||||||
|
self.writer,
|
||||||
|
DirectoryEntry {
|
||||||
|
data_type: value.data_type(),
|
||||||
|
count: K::convert_offset(value.count())?,
|
||||||
|
data: value.as_bytes().into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a directory entry by its byte data.
|
||||||
|
///
|
||||||
|
/// The data must be pre-formatted to the byte order expected by the file.
|
||||||
|
///
|
||||||
|
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
|
||||||
|
/// stored in the entry's offset field.
|
||||||
|
pub fn write_entry_bytes(&mut self, ty: Type, data: &[u8]) -> TiffResult<Entry> {
|
||||||
|
if data.len() % usize::from(ty.byte_len()) != 0 {
|
||||||
|
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
|
||||||
|
ty,
|
||||||
|
found: data.len(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = data.len() / usize::from(ty.byte_len());
|
||||||
|
let count = u64::try_from(count)?;
|
||||||
|
|
||||||
|
Self::write_entry_inner(
|
||||||
|
self.writer,
|
||||||
|
DirectoryEntry {
|
||||||
|
data_type: ty,
|
||||||
|
count: K::convert_offset(count)?,
|
||||||
|
data: data.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert previously written key-value entries.
|
/// Insert previously written key-value entries.
|
||||||
///
|
///
|
||||||
/// It is the caller's responsibility to ensure that the data referred to by the entries is
|
/// It is the caller's responsibility to ensure that the data referred to by the entries is
|
||||||
@@ -374,17 +476,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
Ok(offset)
|
Ok(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_value(
|
// Does it really make sense that this would be a (potentially) compressible write? For actual
|
||||||
|
// directory values their size must match the data written so there's going to be a silent
|
||||||
|
// problem if that actually occurs.
|
||||||
|
fn write_entry_inner(
|
||||||
writer: &mut TiffWriter<W>,
|
writer: &mut TiffWriter<W>,
|
||||||
value: &DirectoryEntry<K::OffsetType>,
|
value: DirectoryEntry<K::OffsetType>,
|
||||||
) -> TiffResult<Entry> {
|
) -> TiffResult<Entry> {
|
||||||
let &DirectoryEntry {
|
let DirectoryEntry {
|
||||||
data: ref bytes,
|
data: ref bytes,
|
||||||
ref count,
|
ref count,
|
||||||
data_type,
|
data_type,
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
let in_entry_bytes = mem::size_of::<K::OffsetType>();
|
let in_entry_bytes = mem::size_of::<K::OffsetType>();
|
||||||
|
|
||||||
let mut offset_bytes = [0; 8];
|
let mut offset_bytes = [0; 8];
|
||||||
|
|
||||||
if bytes.len() > in_entry_bytes {
|
if bytes.len() > in_entry_bytes {
|
||||||
@@ -403,6 +509,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
// and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it.
|
// and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it.
|
||||||
// Instead, write it to a buffer...
|
// Instead, write it to a buffer...
|
||||||
let mut count_bytes = [0; 8];
|
let mut count_bytes = [0; 8];
|
||||||
|
debug_assert!(in_entry_bytes == 4 || in_entry_bytes == 8);
|
||||||
// Nominally Cow but we only expect `Cow::Borrowed`.
|
// Nominally Cow but we only expect `Cow::Borrowed`.
|
||||||
count_bytes[..count.bytes()].copy_from_slice(&count.data());
|
count_bytes[..count.bytes()].copy_from_slice(&count.data());
|
||||||
|
|
||||||
@@ -416,6 +523,15 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_value_byteorder(&self, value: &ValueBuffer) -> TiffResult<()> {
|
||||||
|
// FIXME: we should enable writing files with any chosen byte order.
|
||||||
|
if value.byte_order() != ByteOrder::native() {
|
||||||
|
Err(TiffError::UsageError(UsageError::ByteOrderMismatch))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Provides the number of bytes written by the underlying TiffWriter during the last call.
|
/// Provides the number of bytes written by the underlying TiffWriter during the last call.
|
||||||
fn last_written(&self) -> u64 {
|
fn last_written(&self) -> u64 {
|
||||||
self.writer.last_written()
|
self.writer.last_written()
|
||||||
@@ -565,6 +681,12 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
|
|
||||||
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
|
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(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
|
||||||
|
|
||||||
encoder.write_tag(
|
encoder.write_tag(
|
||||||
@@ -573,7 +695,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
)?;
|
)?;
|
||||||
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
|
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
|
||||||
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
|
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
|
||||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
|
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
|
||||||
|
|
||||||
Ok(ImageEncoder {
|
Ok(ImageEncoder {
|
||||||
encoder,
|
encoder,
|
||||||
@@ -757,7 +879,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
} else {
|
} else {
|
||||||
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
|
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
|
||||||
let replicated =
|
let replicated =
|
||||||
core::iter::repeat(<T>::BITS_PER_SAMPLE[0]).take(self.extra_samples.len());
|
core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
|
||||||
sample_format.extend(replicated);
|
sample_format.extend(replicated);
|
||||||
|
|
||||||
self.encoder
|
self.encoder
|
||||||
@@ -779,7 +901,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
|||||||
// extra samples and not provided via a slice used for samples themselves.
|
// extra samples and not provided via a slice used for samples themselves.
|
||||||
.unwrap_or(SampleFormat::Void.to_u16());
|
.unwrap_or(SampleFormat::Void.to_u16());
|
||||||
|
|
||||||
sample_format.extend(core::iter::repeat(extra_format).take(self.extra_samples.len()));
|
sample_format.extend(core::iter::repeat_n(extra_format, self.extra_samples.len()));
|
||||||
|
|
||||||
self.encoder
|
self.encoder
|
||||||
.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
||||||
@@ -815,10 +937,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DirectoryEntry<S> {
|
struct DirectoryEntry<'data, S> {
|
||||||
data_type: Type,
|
data_type: Type,
|
||||||
count: S,
|
count: S,
|
||||||
data: Vec<u8>,
|
data: std::borrow::Cow<'data, [u8]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait to abstract over Tiff/BigTiff differences.
|
/// Trait to abstract over Tiff/BigTiff differences.
|
||||||
|
|||||||
+165
-1
@@ -1,14 +1,27 @@
|
|||||||
use std::{borrow::Cow, io::Write, slice::from_ref};
|
use std::{borrow::Cow, io::Write, slice::from_ref};
|
||||||
|
|
||||||
use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult};
|
use crate::{bytecast, tags, tags::Type, TiffError, TiffFormatError, TiffResult};
|
||||||
|
|
||||||
use super::writer::TiffWriter;
|
use super::writer::TiffWriter;
|
||||||
|
|
||||||
/// Trait for types that can be encoded in a tiff file
|
/// 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 {
|
pub trait TiffValue {
|
||||||
const BYTE_LEN: u8;
|
const BYTE_LEN: u8;
|
||||||
const FIELD_TYPE: Type;
|
const FIELD_TYPE: Type;
|
||||||
|
|
||||||
|
// FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or
|
||||||
|
// `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX`
|
||||||
|
// for classic tiff.
|
||||||
fn count(&self) -> usize;
|
fn count(&self) -> usize;
|
||||||
|
|
||||||
fn bytes(&self) -> usize {
|
fn bytes(&self) -> usize {
|
||||||
self.count() * usize::from(Self::BYTE_LEN)
|
self.count() * usize::from(Self::BYTE_LEN)
|
||||||
}
|
}
|
||||||
@@ -450,6 +463,136 @@ impl TiffValue for str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: held up on reading the discriminant for all but the unknown variant.
|
||||||
|
//
|
||||||
|
// It needs to have `repr(u16)` to have a defined layout and then we may read the tag from its
|
||||||
|
// value representation by casting a u16 pointer to a pointer to the enum type.
|
||||||
|
// unsafe { *<*const _>::from(self).cast::<u16>() }
|
||||||
|
// But that is quite unsafe and needs careful review to maintain the safety requirements. Maybe we
|
||||||
|
// could get `bytemuck` to add a trait for this (it has `TransparentWrapper`) with a derive for
|
||||||
|
// enum types that have an explicit repr.
|
||||||
|
//
|
||||||
|
// This would allow returning borrowed data in more cases. For all types without an unknown, in all
|
||||||
|
// cases including as slices, and for all types with and unknown variant when we have a single
|
||||||
|
// value (either returning a borrowed discriminant or a borrowed value within `Unknown`).
|
||||||
|
impl TiffValue for tags::CompressionMethod {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
let bytes = self.to_u16().to_ne_bytes();
|
||||||
|
Cow::Owned(bytes.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffValue for tags::PhotometricInterpretation {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
self.to_u16().to_ne_bytes().to_vec().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffValue for tags::PlanarConfiguration {
|
||||||
|
const BYTE_LEN: u8 = 2;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
self.to_u16().to_ne_bytes().to_vec().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffValue for tags::Predictor {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
self.to_u16().to_ne_bytes().to_vec().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffValue for tags::ResolutionUnit {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
self.to_u16().to_ne_bytes().to_vec().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is implemented for slices as the `SampleFormat` tag takes a count of `N`.
|
||||||
|
///
|
||||||
|
/// Use `core::slice::from_ref` if you really need to write a single element.
|
||||||
|
///
|
||||||
|
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
|
||||||
|
impl TiffValue for [tags::SampleFormat] {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
self.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
|
||||||
|
for x in self {
|
||||||
|
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
|
||||||
|
}
|
||||||
|
Cow::Owned(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is implemented for slices as the `ExtraSamples` tag takes a count of `N`.
|
||||||
|
///
|
||||||
|
/// Use `core::slice::from_ref` if you really need to write a single element.
|
||||||
|
///
|
||||||
|
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
|
||||||
|
impl TiffValue for [tags::ExtraSamples] {
|
||||||
|
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
|
||||||
|
const FIELD_TYPE: Type = Type::SHORT;
|
||||||
|
|
||||||
|
fn count(&self) -> usize {
|
||||||
|
self.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> Cow<'_, [u8]> {
|
||||||
|
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
|
||||||
|
for x in self {
|
||||||
|
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
|
||||||
|
}
|
||||||
|
Cow::Owned(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
|
||||||
const BYTE_LEN: u8 = T::BYTE_LEN;
|
const BYTE_LEN: u8 = T::BYTE_LEN;
|
||||||
const FIELD_TYPE: Type = T::FIELD_TYPE;
|
const FIELD_TYPE: Type = T::FIELD_TYPE;
|
||||||
@@ -467,6 +610,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 {
|
macro_rules! impl_tiff_value_for_contiguous_sequence {
|
||||||
($inner_type:ty; $bytes:expr; $field_type:expr) => {
|
($inner_type:ty; $bytes:expr; $field_type:expr) => {
|
||||||
impl $crate::encoder::TiffValue for [$inner_type] {
|
impl $crate::encoder::TiffValue for [$inner_type] {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::string;
|
|||||||
use quick_error::quick_error;
|
use quick_error::quick_error;
|
||||||
|
|
||||||
use crate::decoder::ChunkType;
|
use crate::decoder::ChunkType;
|
||||||
|
use crate::tags::Type;
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
|
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
|
||||||
};
|
};
|
||||||
@@ -90,6 +91,9 @@ quick_error! {
|
|||||||
InvalidTypeForTag {
|
InvalidTypeForTag {
|
||||||
display("tag has invalid type")
|
display("tag has invalid type")
|
||||||
}
|
}
|
||||||
|
InvalidCountForTag(tag: Tag, len: usize) {
|
||||||
|
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
|
||||||
|
}
|
||||||
StripTileTagConflict {
|
StripTileTagConflict {
|
||||||
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
|
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
|
||||||
}
|
}
|
||||||
@@ -156,6 +160,9 @@ quick_error! {
|
|||||||
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
||||||
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
||||||
}
|
}
|
||||||
|
ChromaSubsampling {
|
||||||
|
display("chroma subsampling of YCbCr color is unsupported")
|
||||||
|
}
|
||||||
MisalignedTileBoundaries {
|
MisalignedTileBoundaries {
|
||||||
display("tile rows are not aligned to byte boundaries")
|
display("tile rows are not aligned to byte boundaries")
|
||||||
}
|
}
|
||||||
@@ -202,6 +209,12 @@ quick_error! {
|
|||||||
ReconfiguredAfterImageWrite {
|
ReconfiguredAfterImageWrite {
|
||||||
display("attempted to reconfigure the encoder after image writing has started")
|
display("attempted to reconfigure the encoder after image writing has started")
|
||||||
}
|
}
|
||||||
|
ByteOrderMismatch {
|
||||||
|
display("attempted to use data with a byte order that does not match the required one")
|
||||||
|
}
|
||||||
|
MismatchedEntryLength {ty: Type, found: usize } {
|
||||||
|
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -44,11 +44,15 @@ pub enum ColorType {
|
|||||||
/// Pixel is YCbCr
|
/// Pixel is YCbCr
|
||||||
YCbCr(u8),
|
YCbCr(u8),
|
||||||
|
|
||||||
|
/// Pixel is CIE L*a*b* (ICC LAb)
|
||||||
|
Lab(u8),
|
||||||
|
|
||||||
/// Pixel has multiple bands/channels
|
/// Pixel has multiple bands/channels
|
||||||
Multiband { bit_depth: u8, num_samples: u16 },
|
Multiband { bit_depth: u8, num_samples: u16 },
|
||||||
}
|
}
|
||||||
impl ColorType {
|
impl ColorType {
|
||||||
fn bit_depth(&self) -> u8 {
|
/// The number of bits used to store each sample.
|
||||||
|
pub fn bit_depth(&self) -> u8 {
|
||||||
match *self {
|
match *self {
|
||||||
ColorType::Gray(b)
|
ColorType::Gray(b)
|
||||||
| ColorType::RGB(b)
|
| ColorType::RGB(b)
|
||||||
@@ -58,11 +62,13 @@ impl ColorType {
|
|||||||
| ColorType::CMYK(b)
|
| ColorType::CMYK(b)
|
||||||
| ColorType::CMYKA(b)
|
| ColorType::CMYKA(b)
|
||||||
| ColorType::YCbCr(b)
|
| ColorType::YCbCr(b)
|
||||||
|
| ColorType::Lab(b)
|
||||||
| ColorType::Multiband { bit_depth: b, .. } => b,
|
| ColorType::Multiband { bit_depth: b, .. } => b,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn num_samples(&self) -> u16 {
|
/// The number of samples in each representation of a single color.
|
||||||
|
pub fn num_samples(&self) -> u16 {
|
||||||
match *self {
|
match *self {
|
||||||
ColorType::Gray(_) => 1,
|
ColorType::Gray(_) => 1,
|
||||||
ColorType::RGB(_) => 3,
|
ColorType::RGB(_) => 3,
|
||||||
@@ -72,6 +78,7 @@ impl ColorType {
|
|||||||
ColorType::CMYK(_) => 4,
|
ColorType::CMYK(_) => 4,
|
||||||
ColorType::CMYKA(_) => 5,
|
ColorType::CMYKA(_) => 5,
|
||||||
ColorType::YCbCr(_) => 3,
|
ColorType::YCbCr(_) => 3,
|
||||||
|
ColorType::Lab(_) => 3,
|
||||||
ColorType::Multiband { num_samples, .. } => num_samples,
|
ColorType::Multiband { num_samples, .. } => num_samples,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-8
@@ -1,3 +1,6 @@
|
|||||||
|
use crate::encoder::TiffValue;
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
macro_rules! tags {
|
macro_rules! tags {
|
||||||
{
|
{
|
||||||
// Permit arbitrary meta items, which include documentation.
|
// Permit arbitrary meta items, which include documentation.
|
||||||
@@ -10,8 +13,9 @@ macro_rules! tags {
|
|||||||
$( #[$enum_attr] )*
|
$( #[$enum_attr] )*
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
#[repr($ty)]
|
||||||
pub enum $name {
|
pub enum $name {
|
||||||
$($(#[$ident_attr])* $tag,)*
|
$($(#[$ident_attr])* $tag = $val,)*
|
||||||
$(
|
$(
|
||||||
#[$unknown_meta]
|
#[$unknown_meta]
|
||||||
Unknown($ty),
|
Unknown($ty),
|
||||||
@@ -20,7 +24,7 @@ macro_rules! tags {
|
|||||||
|
|
||||||
impl $name {
|
impl $name {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||||
match n {
|
match n {
|
||||||
$( $val => Ok($name::$tag), )*
|
$( $val => Ok($name::$tag), )*
|
||||||
n => Err(n),
|
n => Err(n),
|
||||||
@@ -28,7 +32,7 @@ macro_rules! tags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn __to_inner_type(&self) -> $ty {
|
const fn __to_inner_type(&self) -> $ty {
|
||||||
match *self {
|
match *self {
|
||||||
$( $name::$tag => $val, )*
|
$( $name::$tag => $val, )*
|
||||||
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
|
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
|
||||||
@@ -42,19 +46,25 @@ macro_rules! tags {
|
|||||||
($name:tt, u16, $($unknown_doc:ident)*) => {
|
($name:tt, u16, $($unknown_doc:ident)*) => {
|
||||||
impl $name {
|
impl $name {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_u16(val: u16) -> Option<Self> {
|
pub const fn from_u16(val: u16) -> Option<Self> {
|
||||||
Self::__from_inner_type(val).ok()
|
match Self::__from_inner_type(val) {
|
||||||
|
Ok(v) => Some(v),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(
|
$(
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
||||||
Self::__from_inner_type($unknown_doc).unwrap_or_else(|_| $name::Unknown($unknown_doc))
|
match Self::__from_inner_type($unknown_doc) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => $name::Unknown($unknown_doc),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_u16(&self) -> u16 {
|
pub const fn to_u16(&self) -> u16 {
|
||||||
Self::__to_inner_type(self)
|
Self::__to_inner_type(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +132,11 @@ pub enum Tag(u16) unknown(
|
|||||||
SMaxSampleValue = 341, // TODO add support
|
SMaxSampleValue = 341, // TODO add support
|
||||||
// JPEG
|
// JPEG
|
||||||
JPEGTables = 347,
|
JPEGTables = 347,
|
||||||
|
// Subsampling
|
||||||
|
#[doc(alias = "YCbCrSubsampling")]
|
||||||
|
ChromaSubsampling = 530, // TODO add support
|
||||||
|
#[doc(alias = "YCbCrPositioning")]
|
||||||
|
ChromaPositioning = 531, // TODO add support
|
||||||
// GeoTIFF
|
// GeoTIFF
|
||||||
ModelPixelScaleTag = 33550, // (SoftDesk)
|
ModelPixelScaleTag = 33550, // (SoftDesk)
|
||||||
ModelTransformationTag = 34264, // (JPL Carto Group)
|
ModelTransformationTag = 34264, // (JPL Carto Group)
|
||||||
@@ -155,6 +170,18 @@ pub enum Tag(u16) unknown(
|
|||||||
// practice it just returns garbage and the validity does not matter greatly to us).
|
// practice it just returns garbage and the validity does not matter greatly to us).
|
||||||
pub struct IfdPointer(pub u64);
|
pub struct IfdPointer(pub u64);
|
||||||
|
|
||||||
|
impl fmt::LowerHex for IfdPointer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt::LowerHex::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::UpperHex for IfdPointer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt::UpperHex::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tags! {
|
tags! {
|
||||||
/// The type of an IFD entry (a 2 byte field).
|
/// The type of an IFD entry (a 2 byte field).
|
||||||
pub enum Type(u16) {
|
pub enum Type(u16) {
|
||||||
@@ -253,6 +280,9 @@ pub enum CompressionMethod(u16) unknown(
|
|||||||
|
|
||||||
// Self-assigned by libtiff
|
// Self-assigned by libtiff
|
||||||
ZSTD = 0xC350,
|
ZSTD = 0xC350,
|
||||||
|
|
||||||
|
// Self-assigned by libtiff
|
||||||
|
WebP = 0xC351,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +296,8 @@ pub enum PhotometricInterpretation(u16) {
|
|||||||
CMYK = 5,
|
CMYK = 5,
|
||||||
YCbCr = 6,
|
YCbCr = 6,
|
||||||
CIELab = 8,
|
CIELab = 8,
|
||||||
|
IccLab = 9,
|
||||||
|
ItuLab = 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +354,124 @@ pub enum ExtraSamples(u16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A value represented as in-memory bytes with flexible byteorder.
|
||||||
|
pub struct ValueBuffer {
|
||||||
|
/// The raw bytes of the value.
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
|
||||||
|
/// The type of the value.
|
||||||
|
ty: Type,
|
||||||
|
|
||||||
|
/// The number of items, as `bytes` may be oversized while holding bytes that are initialized
|
||||||
|
/// but not used by any value.
|
||||||
|
count: u64,
|
||||||
|
|
||||||
|
/// The byte order of the value.
|
||||||
|
byte_order: ByteOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueBuffer {
|
||||||
|
/// A value with a count of zero.
|
||||||
|
///
|
||||||
|
/// The byte order is set to the native byte order of the platform.
|
||||||
|
pub fn empty(ty: Type) -> Self {
|
||||||
|
ValueBuffer {
|
||||||
|
bytes: vec![],
|
||||||
|
ty,
|
||||||
|
count: 0,
|
||||||
|
byte_order: ByteOrder::native(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a value with native byte order from in-memory data.
|
||||||
|
pub fn from_value<T: TiffValue>(value: &T) -> Self {
|
||||||
|
ValueBuffer {
|
||||||
|
bytes: value.data().into_owned(),
|
||||||
|
ty: <T as TiffValue>::FIELD_TYPE,
|
||||||
|
count: value.count() as u64,
|
||||||
|
byte_order: ByteOrder::native(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn byte_order(&self) -> ByteOrder {
|
||||||
|
self.byte_order
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_type(&self) -> Type {
|
||||||
|
self.ty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The count of items in the value.
|
||||||
|
pub fn count(&self) -> u64 {
|
||||||
|
debug_assert!({
|
||||||
|
self.ty
|
||||||
|
.value_bytes(self.count)
|
||||||
|
.is_ok_and(|n| n <= self.bytes.len() as u64)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View the underlying raw bytes of this value.
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.bytes[..self.assumed_len_from_count()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View the underlying mutable raw bytes of this value.
|
||||||
|
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
|
||||||
|
let len = self.assumed_len_from_count();
|
||||||
|
&mut self.bytes[..len]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the byte order of the value representation.
|
||||||
|
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
|
||||||
|
let len = self.assumed_len_from_count();
|
||||||
|
|
||||||
|
self.byte_order
|
||||||
|
.convert(self.ty, &mut self.bytes[..len], byte_order);
|
||||||
|
|
||||||
|
self.byte_order = byte_order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the internal for a value `to_len` bytes long.
|
||||||
|
///
|
||||||
|
/// Shrinks the allocation if it is far too large or extends it if it is too small. In either
|
||||||
|
/// case ensures that at least `to_len` bytes are initialized for [`Self::raw_bytes_mut`].
|
||||||
|
pub(crate) fn prepare_length(&mut self, to_len: usize) {
|
||||||
|
if to_len > self.bytes.len() {
|
||||||
|
self.bytes.resize(to_len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.bytes.len() < to_len / 2 {
|
||||||
|
self.bytes.truncate(to_len);
|
||||||
|
self.bytes.shrink_to_fit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal method to change the type and count while re-interpreting the byte buffer.
|
||||||
|
///
|
||||||
|
/// Should only be called after writing bytes to the internal buffer prepared with
|
||||||
|
/// `Self::prepare_length`.
|
||||||
|
pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) {
|
||||||
|
debug_assert!({
|
||||||
|
ty.value_bytes(count)
|
||||||
|
.is_ok_and(|n| n <= self.bytes.len() as u64)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.byte_order = bo;
|
||||||
|
self.ty = ty;
|
||||||
|
self.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raw_bytes_mut(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assumed_len_from_count(&self) -> usize {
|
||||||
|
usize::from(self.ty.byte_len()) * self.count as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte order of the TIFF file.
|
/// Byte order of the TIFF file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ByteOrder {
|
pub enum ByteOrder {
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ subsubifd.tif:
|
|||||||
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
|
||||||
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
|
||||||
license: CC0
|
license: CC0
|
||||||
|
usda/*.tif
|
||||||
|
url: https://registry.opendata.aws/naip/index.html
|
||||||
|
credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip.
|
||||||
|
license: Public Domain
|
||||||
@@ -7,7 +7,7 @@ use tiff::ColorType;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const TEST_IMAGE_DIR: &str = "./tests/images";
|
const TEST_IMAGE_DIR: &str = "./tests/geotiff";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_geo_tiff() {
|
fn test_geo_tiff() {
|
||||||
@@ -61,3 +61,26 @@ fn test_geo_tiff() {
|
|||||||
assert_eq!(data.len(), 500);
|
assert_eq!(data.len(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "webp")]
|
||||||
|
#[test]
|
||||||
|
fn test_webp_tiff() {
|
||||||
|
let filenames = ["usda_naip_256_webp_z3.tif"];
|
||||||
|
let mut buffer = DecodingResult::U8(vec![]);
|
||||||
|
|
||||||
|
for filename in filenames.iter() {
|
||||||
|
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
|
||||||
|
let img_file = File::open(path).expect("Cannot find test image!");
|
||||||
|
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
|
||||||
|
if !decoder.more_images() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder.next_image().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-1
@@ -1,4 +1,4 @@
|
|||||||
use tiff::decoder::{Decoder, TiffCodingUnit};
|
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit};
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -74,3 +74,50 @@ fn read_first_coding_unit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_planes_by_limits() {
|
||||||
|
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 limits = Limits::default();
|
||||||
|
let mut buffer = DecodingResult::U8(vec![]);
|
||||||
|
|
||||||
|
// Make room for 1 plane.
|
||||||
|
let mut tif = Decoder::new(io).unwrap();
|
||||||
|
tif = tif.with_limits({
|
||||||
|
limits.decoding_buffer_size = 374 * 499;
|
||||||
|
limits.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
|
||||||
|
|
||||||
|
// Make room for 2 planes.
|
||||||
|
tif = tif.with_limits({
|
||||||
|
limits.decoding_buffer_size = 374 * 499 * 2;
|
||||||
|
limits.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
|
||||||
|
|
||||||
|
// Make room for 3 planes.
|
||||||
|
tif = tif.with_limits({
|
||||||
|
limits.decoding_buffer_size = 374 * 499 * 3;
|
||||||
|
limits.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
|
||||||
|
|
||||||
|
// Make room for 4 planes?
|
||||||
|
tif = tif.with_limits({
|
||||||
|
limits.decoding_buffer_size = 374 * 499 * 4;
|
||||||
|
limits.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
tif.read_image_to_buffer(&mut buffer).unwrap();
|
||||||
|
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
|
||||||
|
}
|
||||||
File renamed without changes.
Binary file not shown.
Reference in new issue
Block a user