Compare commits
110
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe305ec24a | ||
|
|
a3ab252168 | ||
|
|
bd64e438a4 | ||
|
|
fa225e820b | ||
|
|
ea373bae00 | ||
|
|
23d1867f75 | ||
|
|
234b2ba677 | ||
|
|
a63a01319c | ||
|
|
efd2a6dbd8 | ||
|
|
d38fba4268 | ||
|
|
1732481b01 | ||
|
|
355ecc8ad9 | ||
|
|
2d90179602 | ||
|
|
5a3b11314e | ||
|
|
5150e91bbb | ||
|
|
6d56d014a1 | ||
|
|
af8a8d660f | ||
|
|
b1c74495ce | ||
|
|
1f02ff8ca1 | ||
|
|
5b9dc30919 | ||
|
|
c3882b252c | ||
|
|
6fe21da979 | ||
|
|
0de1261948 | ||
|
|
6439880e3d | ||
|
|
dce1c40bd5 | ||
|
|
5d3a5242ea | ||
|
|
58543962f3 | ||
|
|
95f2d475fa | ||
|
|
f9eaf9632a | ||
|
|
61b3027f38 | ||
|
|
65662c4834 | ||
|
|
985f3822c8 | ||
|
|
2eef9ac088 | ||
|
|
f6d95249dd | ||
|
|
8517bb81db | ||
|
|
eb6275547d | ||
|
|
73f066c09d | ||
|
|
24887896ff | ||
|
|
02118620e2 | ||
|
|
0b5dc72454 | ||
|
|
34e35fb27e | ||
|
|
bec6558aa8 | ||
|
|
6cdf3d8d22 | ||
|
|
0a2b3f91ab | ||
|
|
99b3c03ce3 | ||
|
|
bec8e495a5 | ||
|
|
de7dbad18b | ||
|
|
19ac30ac3f | ||
|
|
2058b7f3ca | ||
|
|
edd260fe7e | ||
|
|
2ccbe132e0 | ||
|
|
714e470595 | ||
|
|
4adba46b9f | ||
|
|
695208cffe | ||
|
|
4acdfea783 | ||
|
|
64286c2110 | ||
|
|
12c98535f6 | ||
|
|
1556713d6e | ||
|
|
682ef0c375 | ||
|
|
bc71086a96 | ||
|
|
e67bac763f | ||
|
|
839655b975 | ||
|
|
85635c3cd2 | ||
|
|
481a2ab54a | ||
|
|
7c6c327817 | ||
|
|
9f1de91880 | ||
|
|
4a68feab5a | ||
|
|
b5afb8cca4 | ||
|
|
c8d43262e9 | ||
|
|
80ce77190e | ||
|
|
c75aa98ce2 | ||
|
|
c1087f6f08 | ||
|
|
8cc6bc657d | ||
|
|
9fcc2a3b65 | ||
|
|
666f871d30 | ||
|
|
a919b8febf | ||
|
|
85b5df7a62 | ||
|
|
756ea3e813 | ||
|
|
0eead083f6 | ||
|
|
34232d8141 | ||
|
|
cbcf6b97d4 | ||
|
|
19c811e86d | ||
|
|
50be15e356 | ||
|
|
718f2cb455 | ||
|
|
36a16db659 | ||
|
|
d70c31dcbf | ||
|
|
b589d10a27 | ||
|
|
f4378eeb80 | ||
|
|
49015c6e93 | ||
|
|
29402875cb | ||
|
|
33453289b6 | ||
|
|
ce0ca5c234 | ||
|
|
f671293bd8 | ||
|
|
48d52bed9b | ||
|
|
7707060982 | ||
|
|
c370ac4b92 | ||
|
|
b2bcbbaf23 | ||
|
|
d07da5a6cb | ||
|
|
a3c81e9830 | ||
|
|
46e51cc67e | ||
|
|
f6a762b72c | ||
|
|
23672b2490 | ||
|
|
ca74c03520 | ||
|
|
2942f9de97 | ||
|
|
29add10ca7 | ||
|
|
59d32a66a7 | ||
|
|
5a6b2ad8f3 | ||
|
|
c746653269 | ||
|
|
cc75d051af | ||
|
|
2e51ee4b15 |
No files matched your search
@@ -45,6 +45,10 @@ jobs:
|
||||
- name: Cargo check
|
||||
run: cargo check --features "${{ matrix.feature }}" --no-default-features
|
||||
|
||||
- name: Cargo clippy
|
||||
if: ${{ matrix.feature != '' }}
|
||||
run: cargo clippy --no-deps --all-features --all-targets -- -D warnings
|
||||
|
||||
other_archs:
|
||||
# github actions does not support 32-bit or big endian systems directly, but
|
||||
# it does support QEMU. so we install qemu, then build and run the tests on
|
||||
|
||||
+89
@@ -1,3 +1,92 @@
|
||||
# Version 0.11.0
|
||||
|
||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||
|
||||
Changes:
|
||||
- The decoder now interprets the `ExtraSamples` tag. The sample count must now
|
||||
more strict match the expected value with alpha channels only allowing for
|
||||
explicitly denoted unassociated or associated alpha. This effects the
|
||||
indicated color type when decoding images with additional samples indicated
|
||||
as unspecified relation. Previously, these may have been interpreted as
|
||||
alpha by the total sample count (e.g. RgbA if 4 samples under a photometric
|
||||
interpretation of RGB).
|
||||
- The decoder handles planar data, current limited to non-subsampled channels.
|
||||
The `Decoder::read_image` method return planes one-after-another depending on
|
||||
the size of the buffer that was passed.
|
||||
- `Decoder::read_image_to_buffer` now takes `&mut DecodingResult` and resizes
|
||||
it according to the required layout. Previously, a borrowed `DecodingBuffer`
|
||||
was passed which can be replaced by calling `as_bytes_mut` and
|
||||
`read_image_bytes`.
|
||||
- Several methods of tags are now `const`. Note that does not guarantee any
|
||||
particular value when calling these methods.
|
||||
|
||||
Fixes:
|
||||
- Fix a bug in the uncompressed encoder that could lead to short writes, i.e.
|
||||
data silently dropped when the underlying writer did not accept all data in a
|
||||
single write call.
|
||||
- Encoding YCbCr data now writes the `ChromaSubsampling` tag as `(1, 1)` to
|
||||
indicate no subsampling, instead of leaving it at its default of `(2, 2)`.
|
||||
- The decoder will reject subsampled YCbCr data as there is no upsampling
|
||||
routine, except for JPEG compressed images where the JPEG decoders handles
|
||||
this. Since the buffer in that case indicates a full-sized plane for all
|
||||
color samples any future support for the tag will upsample all planes (at
|
||||
least within this major version).
|
||||
|
||||
Additions:
|
||||
- Added support for the `CieLab` color type.
|
||||
- Added `DecodingResult::resize_to` to create a buffer with a matching sample
|
||||
type and dimensions.
|
||||
- Added `ByteOrder::native` to access the platform's native endianness.
|
||||
- Added `ByteOrder::convert` to change the byte-order of values in a byte
|
||||
buffer, depending on their `Type` as described at runtime.
|
||||
- Added `DirectoryOffset::new` to encode a directory whose offset is known to
|
||||
the caller but that has not been written through the encoder itself.
|
||||
- Added `Encoder::extra_samples` to encode images with more samples than their
|
||||
color's trait implementation would otherwise suggest.
|
||||
- Added `DirectoryEncoder::extend_from` to encode multiple tag entries from a
|
||||
directory whose values were written to the file by means outside the
|
||||
encoder's control.
|
||||
- Added `Decoder::read_coding_unit_bytes` to retrieve data of corresponding
|
||||
coordinates from potentially planar data, which is encoded in multiple chunks
|
||||
of the file.
|
||||
|
||||
# Version 0.10.3
|
||||
|
||||
New features:
|
||||
- Group 4 `fax` is now recognized as a compression type and decoded.
|
||||
- A crate feature `fax` controls related dependencies, on by default.
|
||||
|
||||
Changes:
|
||||
- The presence of PhotometricInterpretation::WhiteIsZero now returns an error
|
||||
if the color type can not be inverted by the library where they were
|
||||
previously silently ignored. Please report any missing color types.
|
||||
|
||||
Fixes:
|
||||
- The PhotometricInterpretation::WhiteIsZero is now also applied to 1-bit
|
||||
images, as well as 2-bit and 4-bit sample types where previously the presence
|
||||
of this tag was ignored.
|
||||
|
||||
# Version 0.10.2
|
||||
|
||||
Republished 0.10.1, fixing an accidentally removed method (`Decoder::find_tag_unsigned_vec`).
|
||||
|
||||
# Version 0.10.1
|
||||
|
||||
New features:
|
||||
- Added a dedicated `Directory` type (#277)
|
||||
- Expose methods related to `Directory` publicly (#278)
|
||||
- Implement writing directories (#281, #283)
|
||||
- Add ICC (color profile) to the set of known tags (#287)
|
||||
- Added `Decoder.inner()` to access the underlying reader (#289)
|
||||
- Added methods for reading into a pre-allocated byte buffer (#285)
|
||||
|
||||
Changes:
|
||||
- No longer perform color conversion when decoding embedded JPEG. This fixes decoding for JPEG images with alpha channel. (#274)
|
||||
- More lenient LZW decoding to match libtiff (#231)
|
||||
|
||||
Fixes:
|
||||
- Fixed seeking multiple times in a row (#279)
|
||||
|
||||
# Version 0.10.0
|
||||
|
||||
New features:
|
||||
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
@@ -20,19 +20,24 @@ exclude = ["tests/images/*", "tests/fuzz_images/*"]
|
||||
half = { version = "2.4.1" }
|
||||
quick-error = "2.0.1"
|
||||
|
||||
# Rename to avoid confusion with the feature name.
|
||||
fax34 = { package = "fax", version = "0.2.6", optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
weezl = { version = "0.1.10", optional = true }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
zune-jpeg = { version = "0.4.17", optional = true }
|
||||
zune-jpeg = { version = "0.5.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3.1"
|
||||
# We may already depend on this via flate2
|
||||
crc32fast = "1.5"
|
||||
|
||||
[features]
|
||||
default = ["deflate", "jpeg", "lzw"]
|
||||
default = ["deflate", "fax", "jpeg", "lzw"]
|
||||
|
||||
# Compression algorithms
|
||||
deflate = ["dep:flate2"]
|
||||
fax = ["dep:fax34"]
|
||||
jpeg = ["dep:zune-jpeg"]
|
||||
lzw = ["dep:weezl"]
|
||||
zstd = ["dep:zstd"]
|
||||
|
||||
@@ -36,6 +36,7 @@ This table lists photometric interpretations and sample formats which are suppor
|
||||
| LZW | ✓ | ✓ |
|
||||
| Deflate | ✓ | ✓ |
|
||||
| PackBits | ✓ | ✓ |
|
||||
| Fax4 | ✓ | not yet |
|
||||
| JPEG | ✓ | not yet |
|
||||
| ZSTD | ✓ | not yet |
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use tiff::decoder::{BufferLayoutPreference, Decoder, DecodingBuffer, DecodingResult};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(image) = std::env::args_os().nth(1) else {
|
||||
eprintln!("Usage: decode FILE");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let file = std::fs::File::open(image)?;
|
||||
let io = std::io::BufReader::new(file);
|
||||
let mut reader = Decoder::new(io)?;
|
||||
|
||||
let mut data = DecodingResult::I8(vec![]);
|
||||
|
||||
for i in 0u32.. {
|
||||
let colortype = reader.colortype()?;
|
||||
let dimensions = reader.dimensions()?;
|
||||
let layout = reader.read_image_to_buffer(&mut data)?;
|
||||
|
||||
debug_planes(i, &mut data, &layout, &colortype, dimensions);
|
||||
|
||||
if !reader.more_images() {
|
||||
break;
|
||||
}
|
||||
|
||||
reader.next_image()?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn debug_planes(
|
||||
index: u32,
|
||||
data: &mut DecodingResult,
|
||||
layout: &BufferLayoutPreference,
|
||||
colortype: &tiff::ColorType,
|
||||
(width, height): (u32, u32),
|
||||
) {
|
||||
let (depth, mut tupltype) = match colortype {
|
||||
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
|
||||
tiff::ColorType::RGB(_) => (3, "RGB"),
|
||||
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
|
||||
tiff::ColorType::Palette(_) => (1, "PALETTE"),
|
||||
tiff::ColorType::CMYK(_) => (4, "CMYK"),
|
||||
tiff::ColorType::Multiband { num_samples: 2, .. } => (2, "GRAYSCALE_ALPHA"),
|
||||
_ => {
|
||||
eprintln!("Unsupported color type for PAM output: {:?}", colortype);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let maxval = match data.as_buffer(0) {
|
||||
DecodingBuffer::U8(_) => u64::from(u8::MAX),
|
||||
DecodingBuffer::U16(_) => u64::from(u16::MAX),
|
||||
DecodingBuffer::U32(_) => u64::from(u32::MAX),
|
||||
DecodingBuffer::U64(_) => u64::MAX,
|
||||
// Eh, this is a good effort I guess.
|
||||
DecodingBuffer::I8(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i8::MAX as u8)
|
||||
}
|
||||
DecodingBuffer::I16(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i16::MAX as u16)
|
||||
}
|
||||
DecodingBuffer::I32(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
u64::from(i32::MAX as u32)
|
||||
}
|
||||
DecodingBuffer::I64(items) => {
|
||||
items.iter_mut().for_each(|v| *v = 0.max(*v));
|
||||
i64::MAX as u64
|
||||
}
|
||||
// Not supported by PAM.
|
||||
DecodingBuffer::F16(_) | DecodingBuffer::F32(_) | DecodingBuffer::F64(_) => return,
|
||||
};
|
||||
|
||||
let byte_len = if layout.planes > 1 {
|
||||
tupltype = "GRAYSCALE";
|
||||
layout.plane_stride.unwrap().get()
|
||||
} else {
|
||||
data.as_buffer(0).byte_len()
|
||||
};
|
||||
|
||||
// Write out this image's planes to PAM files.
|
||||
let header = format!("P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH {depth}\nMAXVAL {maxval}\nTUPLTYPE {tupltype}\nENDHDR\n");
|
||||
|
||||
for (plane, plane_data) in data
|
||||
.as_buffer(0)
|
||||
.as_bytes_mut()
|
||||
.chunks_exact(byte_len)
|
||||
.enumerate()
|
||||
{
|
||||
let filename = format!("image_{index:03}_plane_{plane:03}.pam");
|
||||
let mut file = std::fs::File::create(filename).expect("Failed to create output file");
|
||||
|
||||
use std::io::Write;
|
||||
file.write_all(header.as_bytes())
|
||||
.expect("Failed to write PAM header");
|
||||
file.write_all(plane_data)
|
||||
.expect("Failed to write PAM data");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@
|
||||
//! the unsafe code guidelines).
|
||||
//!
|
||||
//! TODO: Would like to use std-lib here.
|
||||
// Until we implement predictors for f16, we don't need f16_as_ne_bytes. (Due to the macro we do
|
||||
// not apply this directly to the functions). And rust-version is not new enough for `expect`.
|
||||
#![allow(dead_code)]
|
||||
use std::{mem, slice};
|
||||
|
||||
use half::f16;
|
||||
|
||||
+227
-121
@@ -332,6 +332,12 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// A combination of type, count, and offset.
|
||||
///
|
||||
/// In a TIFF the data offset portion of an entry is used for inline data in case the length of the
|
||||
/// encoded value does not exceed the size of the offset field. Since the size of the offset field
|
||||
/// depends on the file kind (4 bytes for standard TIFF, 8 bytes for BigTIFF) the interpretation of
|
||||
/// this struct is only complete in combination with file metadata.
|
||||
#[derive(Clone)]
|
||||
pub struct Entry {
|
||||
type_: Type,
|
||||
@@ -349,12 +355,14 @@ impl ::std::fmt::Debug for Entry {
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Create a new entry fit to be added to a standard TIFF IFD.
|
||||
pub fn new(type_: Type, count: u32, offset: [u8; 4]) -> Entry {
|
||||
let mut entry_off = [0u8; 8];
|
||||
entry_off[..4].copy_from_slice(&offset);
|
||||
Entry::new_u64(type_, count.into(), entry_off)
|
||||
}
|
||||
|
||||
/// Create a new entry with data for a Big TIFF IFD.
|
||||
pub fn new_u64(type_: Type, count: u64, offset: [u8; 8]) -> Entry {
|
||||
Entry {
|
||||
type_,
|
||||
@@ -376,7 +384,10 @@ impl Entry {
|
||||
}
|
||||
|
||||
/// Returns a mem_reader for the offset/value field
|
||||
fn r(&self, byte_order: ByteOrder) -> EndianReader<io::Cursor<Vec<u8>>> {
|
||||
pub(crate) fn offset_field_reader(
|
||||
&self,
|
||||
byte_order: ByteOrder,
|
||||
) -> EndianReader<io::Cursor<Vec<u8>>> {
|
||||
EndianReader::new(io::Cursor::new(self.offset.to_vec()), byte_order)
|
||||
}
|
||||
|
||||
@@ -392,43 +403,25 @@ impl Entry {
|
||||
}
|
||||
|
||||
let bo = reader.byte_order;
|
||||
|
||||
let tag_size = match self.type_ {
|
||||
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
|
||||
Type::SHORT | Type::SSHORT => 2,
|
||||
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
|
||||
Type::LONG8
|
||||
| Type::SLONG8
|
||||
| Type::DOUBLE
|
||||
| Type::RATIONAL
|
||||
| Type::SRATIONAL
|
||||
| Type::IFD8 => 8,
|
||||
};
|
||||
|
||||
let value_bytes = match self.count.checked_mul(tag_size) {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
};
|
||||
let value_bytes = self.type_.value_bytes(self.count)?;
|
||||
|
||||
// Case 2: there is one value.
|
||||
if self.count == 1 {
|
||||
// 2a: the value is 5-8 bytes and we're in BigTiff mode.
|
||||
if bigtiff && value_bytes > 4 && value_bytes <= 8 {
|
||||
return Ok(match self.type_ {
|
||||
Type::LONG8 => UnsignedBig(self.r(bo).read_u64()?),
|
||||
Type::SLONG8 => SignedBig(self.r(bo).read_i64()?),
|
||||
Type::DOUBLE => Double(self.r(bo).read_f64()?),
|
||||
Type::LONG8 => UnsignedBig(self.offset_field_reader(bo).read_u64()?),
|
||||
Type::SLONG8 => SignedBig(self.offset_field_reader(bo).read_i64()?),
|
||||
Type::DOUBLE => Double(self.offset_field_reader(bo).read_f64()?),
|
||||
Type::RATIONAL => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
Rational(r.read_u32()?, r.read_u32()?)
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
SRational(r.read_i32()?, r.read_i32()?)
|
||||
}
|
||||
Type::IFD8 => IfdBig(self.r(bo).read_u64()?),
|
||||
Type::IFD8 => IfdBig(self.offset_field_reader(bo).read_u64()?),
|
||||
Type::BYTE
|
||||
| Type::SBYTE
|
||||
| Type::ASCII
|
||||
@@ -445,13 +438,13 @@ impl Entry {
|
||||
// 2b: the value is at most 4 bytes or doesn't fit in the offset field.
|
||||
return Ok(match self.type_ {
|
||||
Type::BYTE => Byte(self.offset[0]),
|
||||
Type::SBYTE => SignedByte(i8::from(self.offset[0] as i8)),
|
||||
Type::SBYTE => SignedByte(self.offset[0] as i8),
|
||||
Type::UNDEFINED => Byte(self.offset[0]),
|
||||
Type::SHORT => Short(self.r(bo).read_u16()?),
|
||||
Type::SSHORT => SignedShort(self.r(bo).read_i16()?),
|
||||
Type::LONG => Unsigned(self.r(bo).read_u32()?),
|
||||
Type::SLONG => Signed(self.r(bo).read_i32()?),
|
||||
Type::FLOAT => Float(self.r(bo).read_f32()?),
|
||||
Type::SHORT => Short(self.offset_field_reader(bo).read_u16()?),
|
||||
Type::SSHORT => SignedShort(self.offset_field_reader(bo).read_i16()?),
|
||||
Type::LONG => Unsigned(self.offset_field_reader(bo).read_u32()?),
|
||||
Type::SLONG => Signed(self.offset_field_reader(bo).read_i32()?),
|
||||
Type::FLOAT => Float(self.offset_field_reader(bo).read_f32()?),
|
||||
Type::ASCII => {
|
||||
if self.offset[0] == 0 {
|
||||
Ascii("".to_string())
|
||||
@@ -460,28 +453,28 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
Type::LONG8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
UnsignedBig(reader.read_u64()?)
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
SignedBig(reader.read_i64()?)
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
Double(reader.read_f64()?)
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
Rational(reader.read_u32()?, reader.read_u32()?)
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
SRational(reader.read_i32()?, reader.read_i32()?)
|
||||
}
|
||||
Type::IFD => Ifd(self.r(bo).read_u32()?),
|
||||
Type::IFD => Ifd(self.offset_field_reader(bo).read_u32()?),
|
||||
Type::IFD8 => {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?;
|
||||
reader.goto_offset(self.offset_field_reader(bo).read_u32()?.into())?;
|
||||
IfdBig(reader.read_u64()?)
|
||||
}
|
||||
});
|
||||
@@ -512,7 +505,7 @@ impl Entry {
|
||||
));
|
||||
}
|
||||
Type::SHORT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Short(r.read_u16()?));
|
||||
@@ -520,7 +513,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::SSHORT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(SignedShort(r.read_i16()?));
|
||||
@@ -528,7 +521,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::LONG => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Unsigned(r.read_u32()?));
|
||||
@@ -536,7 +529,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::SLONG => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Signed(r.read_i32()?));
|
||||
@@ -544,7 +537,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::FLOAT => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Float(r.read_f32()?));
|
||||
@@ -552,7 +545,7 @@ impl Entry {
|
||||
return Ok(List(v));
|
||||
}
|
||||
Type::IFD => {
|
||||
let mut r = self.r(bo);
|
||||
let mut r = self.offset_field_reader(bo);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.count {
|
||||
v.push(Ifd(r.read_u32()?));
|
||||
@@ -571,74 +564,149 @@ impl Entry {
|
||||
}
|
||||
|
||||
// Case 4: there is more than one value, and it doesn't fit in the offset field.
|
||||
let mut v;
|
||||
self.set_reader_offset(bo, bigtiff, reader)?;
|
||||
|
||||
match self.type_ {
|
||||
// TODO check if this could give wrong results
|
||||
// at a different endianess of file/computer.
|
||||
Type::BYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
let mut buf = [0; 1];
|
||||
reader.inner().read_exact(&mut buf)?;
|
||||
Ok(UnsignedBig(u64::from(buf[0])))
|
||||
}),
|
||||
Type::SBYTE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedBig(i64::from(reader.read_i8()?)))
|
||||
}),
|
||||
Type::SHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Short(reader.read_u16()?))
|
||||
}),
|
||||
Type::SSHORT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedShort(reader.read_i16()?))
|
||||
}),
|
||||
Type::LONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Unsigned(reader.read_u32()?))
|
||||
}),
|
||||
Type::SLONG => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Signed(reader.read_i32()?))
|
||||
}),
|
||||
Type::FLOAT => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Float(reader.read_f32()?))
|
||||
}),
|
||||
Type::DOUBLE => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Double(reader.read_f64()?))
|
||||
}),
|
||||
Type::BYTE | Type::UNDEFINED => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(Byte))
|
||||
})
|
||||
}
|
||||
Type::SBYTE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.iter().copied().map(|v| SignedByte(v as i8)))
|
||||
})
|
||||
}
|
||||
Type::SHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(2)
|
||||
.map(|ch| Short(u16::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SSHORT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(2)
|
||||
.map(|ch| SignedShort(i16::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::LONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Unsigned(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Signed(i32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::FLOAT => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Float(f32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::DOUBLE => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| Double(f64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::RATIONAL => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Rational(reader.read_u32()?, reader.read_u32()?))
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
Rational(
|
||||
u32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
u32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Type::SRATIONAL => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SRational(reader.read_i32()?, reader.read_i32()?))
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(bytes.chunks_exact(8).map(|ch| {
|
||||
SRational(
|
||||
i32::from_ne_bytes(ch[..4].try_into().unwrap()),
|
||||
i32::from_ne_bytes(ch[4..].try_into().unwrap()),
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Type::LONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(UnsignedBig(reader.read_u64()?))
|
||||
}),
|
||||
Type::SLONG8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(SignedBig(reader.read_i64()?))
|
||||
}),
|
||||
Type::IFD => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(Ifd(reader.read_u32()?))
|
||||
}),
|
||||
Type::IFD8 => self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
Ok(IfdBig(reader.read_u64()?))
|
||||
}),
|
||||
Type::UNDEFINED => {
|
||||
self.decode_offset(self.count, bo, bigtiff, limits, reader, |reader| {
|
||||
let mut buf = [0; 1];
|
||||
reader.inner().read_exact(&mut buf)?;
|
||||
Ok(Byte(buf[0]))
|
||||
Type::LONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| UnsignedBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::SLONG8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| SignedBig(i64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|ch| Ifd(u32::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::IFD8 => {
|
||||
v = Self::vec_with_capacity(self.count, limits)?;
|
||||
self.decode_values(self.count, self.type_, reader, |bytes| {
|
||||
v.extend(
|
||||
bytes
|
||||
.chunks_exact(8)
|
||||
.map(|ch| IfdBig(u64::from_ne_bytes(ch.try_into().unwrap()))),
|
||||
)
|
||||
})
|
||||
}
|
||||
Type::ASCII => {
|
||||
let n = usize::try_from(self.count)?;
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
if bigtiff {
|
||||
reader.goto_offset(self.r(bo).read_u64()?)?
|
||||
} else {
|
||||
reader.goto_offset(self.r(bo).read_u32()?.into())?
|
||||
if n > limits.decoding_buffer_size {
|
||||
return Err(dbg!(TiffError::LimitsExceeded));
|
||||
}
|
||||
|
||||
let mut out = vec![0; n];
|
||||
@@ -647,43 +715,81 @@ impl Entry {
|
||||
if let Some(first) = out.iter().position(|&b| b == 0) {
|
||||
out.truncate(first);
|
||||
}
|
||||
Ok(Ascii(String::from_utf8(out)?))
|
||||
|
||||
return Ok(Ascii(String::from_utf8(out)?));
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(List(v))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decode_offset<R, F>(
|
||||
&self,
|
||||
fn vec_with_capacity(
|
||||
value_count: u64,
|
||||
bo: ByteOrder,
|
||||
bigtiff: bool,
|
||||
limits: &super::Limits,
|
||||
reader: &mut EndianReader<R>,
|
||||
decode_fn: F,
|
||||
) -> TiffResult<Value>
|
||||
where
|
||||
R: Read + Seek,
|
||||
F: Fn(&mut EndianReader<R>) -> TiffResult<Value>,
|
||||
{
|
||||
) -> Result<Vec<Value>, TiffError> {
|
||||
let value_count = usize::try_from(value_count)?;
|
||||
|
||||
if value_count > limits.decoding_buffer_size / mem::size_of::<Value>() {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
|
||||
let mut v = Vec::with_capacity(value_count);
|
||||
Ok(Vec::with_capacity(value_count))
|
||||
}
|
||||
|
||||
fn set_reader_offset<R>(
|
||||
&self,
|
||||
bo: ByteOrder,
|
||||
bigtiff: bool,
|
||||
reader: &mut EndianReader<R>,
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let offset = if bigtiff {
|
||||
self.r(bo).read_u64()?
|
||||
self.offset_field_reader(bo).read_u64()?
|
||||
} else {
|
||||
self.r(bo).read_u32()?.into()
|
||||
self.offset_field_reader(bo).read_u32()?.into()
|
||||
};
|
||||
|
||||
reader.goto_offset(offset)?;
|
||||
|
||||
for _ in 0..value_count {
|
||||
v.push(decode_fn(reader)?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decode_values<R, F>(
|
||||
&self,
|
||||
value_count: u64,
|
||||
type_: Type,
|
||||
reader: &mut EndianReader<R>,
|
||||
mut collect: F,
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
F: FnMut(&[u8]),
|
||||
{
|
||||
let mut total_bytes = type_.value_bytes(value_count)?;
|
||||
let mut buffer = [0u8; 512];
|
||||
|
||||
let buf_unit = usize::from(type_.byte_len());
|
||||
let mul_of_ty = buffer.len() / buf_unit * buf_unit;
|
||||
|
||||
let cls = type_.endian_bytes();
|
||||
let native = ByteOrder::native();
|
||||
|
||||
while total_bytes > 0 {
|
||||
// `now <= mul_of_ty < 512` so casting is mathematical
|
||||
let now = total_bytes.min(mul_of_ty as u64);
|
||||
total_bytes -= now;
|
||||
|
||||
let buffer = &mut buffer[..now as usize];
|
||||
reader.inner().read_exact(buffer)?;
|
||||
|
||||
reader.byte_order.convert_endian_bytes(cls, buffer, native);
|
||||
collect(buffer);
|
||||
}
|
||||
Ok(List(v))
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+513
-61
@@ -4,11 +4,13 @@ use super::tag_reader::TagReader;
|
||||
use super::ChunkType;
|
||||
use super::{predict_f16, predict_f32, predict_f64, ValueReader};
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
|
||||
CompressionMethod, ExtraSamples, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||
SampleFormat, Tag,
|
||||
};
|
||||
use crate::{
|
||||
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
|
||||
};
|
||||
|
||||
use std::io::{self, Cursor, Read, Seek};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -29,10 +31,10 @@ pub(crate) struct TileAttributes {
|
||||
|
||||
impl TileAttributes {
|
||||
pub fn tiles_across(&self) -> usize {
|
||||
(self.image_width + self.tile_width - 1) / self.tile_width
|
||||
self.image_width.div_ceil(self.tile_width)
|
||||
}
|
||||
pub fn tiles_down(&self) -> usize {
|
||||
(self.image_height + self.tile_length - 1) / self.tile_length
|
||||
self.image_height.div_ceil(self.tile_length)
|
||||
}
|
||||
fn padding_right(&self) -> usize {
|
||||
(self.tile_width - self.image_width % self.tile_width) % self.tile_width
|
||||
@@ -67,6 +69,11 @@ pub(crate) struct Image {
|
||||
pub height: u32,
|
||||
pub bits_per_sample: u8,
|
||||
pub samples: u16,
|
||||
/// The `ExtraSamples`, defaulting to empty if not given.
|
||||
pub extra_samples: Vec<ExtraSamples>,
|
||||
/// Number of samples that belong to the photometric interpretation, samples except
|
||||
/// `ExtraSamples` (338, 0x0152) tag.
|
||||
pub photometric_samples: u16,
|
||||
pub sample_format: SampleFormat,
|
||||
pub photometric_interpretation: PhotometricInterpretation,
|
||||
pub compression_method: CompressionMethod,
|
||||
@@ -78,6 +85,58 @@ pub(crate) struct Image {
|
||||
pub tile_attributes: Option<TileAttributes>,
|
||||
pub chunk_offsets: Vec<u64>,
|
||||
pub chunk_bytes: Vec<u64>,
|
||||
pub chroma_subsampling: (u16, u16),
|
||||
}
|
||||
|
||||
/// Describes how to read a tile-aligned portion of the image.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReadoutLayout {
|
||||
/// The planar configuration, which applies to both the underlying image and the output buffer.
|
||||
/// This may be relaxed if we find a clean enough way to provide it.
|
||||
pub planar_config: PlanarConfiguration,
|
||||
|
||||
/// The sample interpretation (interpret with planar_config).
|
||||
///
|
||||
/// FIXME: we should not require this here. The ability to turn out the raw bytes from the
|
||||
/// sample arrays is very different from turning out interpretable color. Firstly we can always
|
||||
/// readout `Multiband` but currently only use that ColorType in special circumstances (it must
|
||||
/// not overlap cases where actually want to use a ColorType).
|
||||
///
|
||||
/// And then we have CIE Lab, which uses a tuple of `(u8, i8, i8)`, that is still filterable
|
||||
/// but still not represented by any of our `DecoderResult` variants. Other color variants
|
||||
/// depend on extra tags (YCbCrCoefficients/0x0211) and we don't have a good side channel to
|
||||
/// tag the output with all that TIFF specific information, so arguably we should process and
|
||||
/// apply those to the data so it becomes a self-contained representation.
|
||||
///
|
||||
/// This should be computed at a higher level, in `Decoder`, instead.
|
||||
pub color: ColorType,
|
||||
/// The number of bytes from one row to another.
|
||||
pub minimum_row_stride: usize,
|
||||
/// The format of samples (assumed uniform for now, same with depth of `ColorType`).
|
||||
pub sample_format: SampleFormat,
|
||||
|
||||
/// Number of bytes to advance in output per row.
|
||||
pub row_stride: usize,
|
||||
/// Number of bytes to advance in output per chunk in width.
|
||||
pub chunk_row_stride: usize,
|
||||
/// Number of bytes to advance in output per chunk in height.
|
||||
pub chunk_col_stride: usize,
|
||||
/// Number of bytes in output from one plane to another.
|
||||
pub plane_stride: usize,
|
||||
|
||||
/// Bits per sample in the encoded data.
|
||||
pub tiff_bits_per_sample: u8,
|
||||
/// Number of samples in the encoded data.
|
||||
pub tiff_samples: u16,
|
||||
/// Dimensions of the underlying rectangular chunks (tile or strips).
|
||||
pub tiff_chunk_dimensions: (u32, u32),
|
||||
/// Number of bytes in the underlying data with all samples per row of chunks.
|
||||
pub tiff_row_bytes: usize,
|
||||
|
||||
/// Chunks until wrapping to the next row of chunks.
|
||||
pub chunks_across: u32,
|
||||
/// Chunks to advance to get to the next plane of chunks.
|
||||
pub chunks_per_plane: u32,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
@@ -132,10 +191,30 @@ impl Image {
|
||||
.map(Value::into_u16)
|
||||
.transpose()?
|
||||
.unwrap_or(1);
|
||||
|
||||
if samples == 0 {
|
||||
return Err(TiffFormatError::SamplesPerPixelIsZero.into());
|
||||
}
|
||||
|
||||
let extra_samples = match tag_reader.find_tag(Tag::ExtraSamples)? {
|
||||
Some(n) => n.into_u16_vec()?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let extra_samples = extra_samples
|
||||
.into_iter()
|
||||
.map(|x| ExtraSamples::from_u16(x).unwrap_or(ExtraSamples::Unspecified))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let photometric_samples = match usize::from(samples).checked_sub(extra_samples.len()) {
|
||||
None => {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
));
|
||||
}
|
||||
Some(n) => n as u16,
|
||||
};
|
||||
|
||||
let sample_format = match tag_reader.find_tag_uint_vec(Tag::SampleFormat)? {
|
||||
Some(vals) => {
|
||||
let sample_format: Vec<_> = vals
|
||||
@@ -143,12 +222,15 @@ impl Image {
|
||||
.map(SampleFormat::from_u16_exhaustive)
|
||||
.collect();
|
||||
|
||||
let Some(format) = sample_format.first().copied() else {
|
||||
// Reject empty sample formats
|
||||
return Err(TiffFormatError::InvalidTagValueType(Tag::SampleFormat).into());
|
||||
};
|
||||
// TODO: for now, only homogenous formats across samples are supported.
|
||||
if !sample_format.windows(2).all(|s| s[0] == s[1]) {
|
||||
if !sample_format.iter().all(|&s| s == format) {
|
||||
return Err(TiffUnsupportedError::UnsupportedSampleFormat(sample_format).into());
|
||||
}
|
||||
|
||||
sample_format[0]
|
||||
format
|
||||
}
|
||||
None => SampleFormat::Uint,
|
||||
};
|
||||
@@ -159,7 +241,7 @@ impl Image {
|
||||
|
||||
// Technically bits_per_sample.len() should be *equal* to samples, but libtiff also allows
|
||||
// it to be a single value that applies to all samples.
|
||||
if bits_per_sample.len() != samples.into() && bits_per_sample.len() != 1 {
|
||||
if bits_per_sample.len() != usize::from(samples) && bits_per_sample.len() != 1 {
|
||||
return Err(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
));
|
||||
@@ -194,6 +276,25 @@ impl Image {
|
||||
.transpose()?
|
||||
.unwrap_or(PlanarConfiguration::Chunky);
|
||||
|
||||
let ycbcr_subsampling = tag_reader.find_tag_uint_vec::<u16>(Tag::ChromaSubsampling)?;
|
||||
|
||||
let chroma_subsampling = if let Some(subsamples) = &ycbcr_subsampling {
|
||||
let [a, b] = subsamples.as_slice() else {
|
||||
return Err(TiffError::FormatError(TiffFormatError::InvalidCountForTag(
|
||||
Tag::ChromaSubsampling,
|
||||
subsamples.len(),
|
||||
)));
|
||||
};
|
||||
|
||||
// ImageWidth and ImageLength are constrained to be integer multiples of
|
||||
// YCbCrSubsampleHoriz and YCbCrSubsampleVert respectively. TileWidth and TileLength
|
||||
// have the same constraints. RowsPerStrip must be an integer multiple of
|
||||
// YCbCrSubsampleVert.
|
||||
(*a, *b)
|
||||
} else {
|
||||
(2, 2)
|
||||
};
|
||||
|
||||
let planes = match planar_config {
|
||||
PlanarConfiguration::Chunky => 1,
|
||||
PlanarConfiguration::Planar => samples,
|
||||
@@ -292,6 +393,8 @@ impl Image {
|
||||
height,
|
||||
bits_per_sample: bits_per_sample[0],
|
||||
samples,
|
||||
extra_samples,
|
||||
photometric_samples,
|
||||
sample_format,
|
||||
photometric_interpretation,
|
||||
compression_method,
|
||||
@@ -303,19 +406,24 @@ impl Image {
|
||||
tile_attributes,
|
||||
chunk_offsets,
|
||||
chunk_bytes,
|
||||
chroma_subsampling,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn colortype(&self) -> TiffResult<ColorType> {
|
||||
let is_alpha_extra_samples = matches!(
|
||||
self.extra_samples.as_slice(),
|
||||
[ExtraSamples::AssociatedAlpha, ..] | [ExtraSamples::UnassociatedAlpha, ..]
|
||||
);
|
||||
|
||||
match self.photometric_interpretation {
|
||||
PhotometricInterpretation::RGB => match self.samples {
|
||||
3 => Ok(ColorType::RGB(self.bits_per_sample)),
|
||||
PhotometricInterpretation::RGB => match self.photometric_samples {
|
||||
3 => Ok(if is_alpha_extra_samples {
|
||||
ColorType::RGBA(self.bits_per_sample)
|
||||
} else {
|
||||
ColorType::RGB(self.bits_per_sample)
|
||||
}),
|
||||
4 => Ok(ColorType::RGBA(self.bits_per_sample)),
|
||||
// FIXME: We should _ignore_ other components. In particular:
|
||||
// > Beware of extra components. Some TIFF files may have more components per pixel
|
||||
// than you think. A Baseline TIFF reader must skip over them gracefully,using the
|
||||
// values of the SamplesPerPixel and BitsPerSample fields.
|
||||
// > -- TIFF 6.0 Specification, Section 7, Additional Baseline requirements.
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
@@ -323,8 +431,12 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
PhotometricInterpretation::CMYK => match self.samples {
|
||||
4 => Ok(ColorType::CMYK(self.bits_per_sample)),
|
||||
PhotometricInterpretation::CMYK => match self.photometric_samples {
|
||||
4 => Ok(if is_alpha_extra_samples {
|
||||
ColorType::CMYKA(self.bits_per_sample)
|
||||
} else {
|
||||
ColorType::CMYK(self.bits_per_sample)
|
||||
}),
|
||||
5 => Ok(ColorType::CMYKA(self.bits_per_sample)),
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
@@ -333,7 +445,7 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
PhotometricInterpretation::YCbCr => match self.samples {
|
||||
PhotometricInterpretation::YCbCr => match self.photometric_samples {
|
||||
3 => Ok(ColorType::YCbCr(self.bits_per_sample)),
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
@@ -342,7 +454,13 @@ impl Image {
|
||||
),
|
||||
)),
|
||||
},
|
||||
// TODO: treatment of WhiteIsZero is not quite consistent with `invert_colors` that is
|
||||
// later called when that interpretation is read. That function does not support
|
||||
// Multiband as a color type and will error. It's unclear how to resolve that exactly.
|
||||
PhotometricInterpretation::BlackIsZero | PhotometricInterpretation::WhiteIsZero => {
|
||||
// Note: compatibility with previous implementation requires us to return extra
|
||||
// samples as `Multiband`. For gray images however the better choice would be
|
||||
// returning a `Gray` color, i.e. matching on `photometric_samples` instead.
|
||||
match self.samples {
|
||||
1 => Ok(ColorType::Gray(self.bits_per_sample)),
|
||||
_ => Ok(ColorType::Multiband {
|
||||
@@ -351,15 +469,60 @@ impl Image {
|
||||
}),
|
||||
}
|
||||
}
|
||||
// TODO: this is bad we should not fail at this point
|
||||
PhotometricInterpretation::RGBPalette
|
||||
| PhotometricInterpretation::TransparencyMask
|
||||
| PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
// ```
|
||||
// struct IccLab /* Interpretation 9* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: u8, // SampleFormat::Uint, defined as TiffLab::a + 128
|
||||
// pub b: u8, // SampleFormat::Uint, defined as TiffLab::b + 128
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::IccLab => match self.photometric_samples {
|
||||
3 if matches!(self.sample_format, SampleFormat::Uint) => {
|
||||
Ok(ColorType::Lab(self.bits_per_sample))
|
||||
}
|
||||
_ => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
},
|
||||
// Unsupported due to inherently heterogeneous sample types. This is represented as:
|
||||
// ```
|
||||
// struct TiffLab /* Interpretation 8* {
|
||||
// pub L: u8, // SampleFormat::Uint
|
||||
// pub a: i8, // SampleFormat::Int
|
||||
// pub b: i8, // SampleFormat::Int
|
||||
// }
|
||||
// ```
|
||||
PhotometricInterpretation::CIELab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
// Unsupported due to extra unfiltering and conversion steps. We need to find the
|
||||
// Decode tag (SRATIONAL; 2 * SamplesPerPixel) and apply the following conversion:
|
||||
//
|
||||
// L* = Decode[0] + Lsample x (Decode[1] - Decode[0]) / (2^n -1)
|
||||
// …
|
||||
//
|
||||
// So we'll have a larger depth in the output and either worry about reducing fractions
|
||||
// or turn everything into floats. That's a lot of decisions.
|
||||
PhotometricInterpretation::ItuLab => Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
PhotometricInterpretation::CIELab,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
)),
|
||||
PhotometricInterpretation::RGBPalette | PhotometricInterpretation::TransparencyMask => {
|
||||
Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +530,9 @@ impl Image {
|
||||
reader: R,
|
||||
compression_method: CompressionMethod,
|
||||
compressed_length: u64,
|
||||
#[allow(unused_variables)] jpeg_tables: Option<&[u8]>,
|
||||
// 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 = "fax"), allow(unused_variables))] dimensions: (u32, u32),
|
||||
) -> TiffResult<Box<dyn Read + 'r>> {
|
||||
Ok(match compression_method {
|
||||
CompressionMethod::None => Box::new(reader),
|
||||
@@ -419,13 +584,14 @@ impl Image {
|
||||
let mut jpeg_data = Vec::new();
|
||||
jpeg_reader.read_to_end(&mut jpeg_data)?;
|
||||
|
||||
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg_data);
|
||||
let mut decoder =
|
||||
zune_jpeg::JpegDecoder::new(zune_core::bytestream::ZCursor::new(jpeg_data));
|
||||
let mut options: zune_core::options::DecoderOptions = Default::default();
|
||||
|
||||
// Disable color conversion by setting the output colorspace to the input
|
||||
// colorspace.
|
||||
decoder.decode_headers()?;
|
||||
if let Some(colorspace) = decoder.get_input_colorspace() {
|
||||
if let Some(colorspace) = decoder.input_colorspace() {
|
||||
options = options.jpeg_set_out_colorspace(colorspace);
|
||||
}
|
||||
|
||||
@@ -435,6 +601,12 @@ impl Image {
|
||||
|
||||
Box::new(Cursor::new(data))
|
||||
}
|
||||
#[cfg(feature = "fax")]
|
||||
CompressionMethod::Fax4 => Box::new(super::stream::Group4Reader::new(
|
||||
dimensions,
|
||||
reader,
|
||||
compressed_length,
|
||||
)?),
|
||||
method => {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::UnsupportedCompressionMethod(method),
|
||||
@@ -450,18 +622,25 @@ impl Image {
|
||||
/// Example with `bits_per_sample = [8, 8, 8]` and `PhotometricInterpretation::RGB`:
|
||||
/// * `PlanarConfiguration::Chunky` -> 3 (RGBRGBRGB...)
|
||||
/// * `PlanarConfiguration::Planar` -> 1 (RRR...) (GGG...) (BBB...)
|
||||
pub(crate) fn samples_per_pixel(&self) -> usize {
|
||||
pub(crate) fn samples_per_pixel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => self.samples.into(),
|
||||
PlanarConfiguration::Chunky => self.samples,
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn samples_per_out_texel(&self, color: ColorType) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => color.num_samples(),
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of strips per pixel.
|
||||
pub(crate) fn strips_per_pixel(&self) -> usize {
|
||||
pub(crate) fn strips_per_pixel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => 1,
|
||||
PlanarConfiguration::Planar => self.samples.into(),
|
||||
PlanarConfiguration::Planar => self.samples,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,14 +678,107 @@ impl Image {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn readout_for_image(&self) -> TiffResult<ReadoutLayout> {
|
||||
let Image { width, height, .. } = *self;
|
||||
self.readout_for_size(width, height)
|
||||
}
|
||||
|
||||
/// Get the layout for reading out a tile-aligned portion of the image.
|
||||
///
|
||||
/// The provided width and height should be less than or equal to the image dimensions.
|
||||
pub(crate) fn readout_for_size(&self, width: u32, height: u32) -> TiffResult<ReadoutLayout> {
|
||||
let color = self.colortype()?;
|
||||
|
||||
let tiff_samples = self.samples_per_pixel();
|
||||
let tiff_bits_per_sample = self.bits_per_sample;
|
||||
let data_samples = self.samples_per_out_texel(color);
|
||||
let tiff_chunk_dimensions = self.chunk_dimensions()?;
|
||||
let strips_per_pixel = self.strips_per_pixel();
|
||||
|
||||
let data_dimensions = (width, height);
|
||||
|
||||
let tiff_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(tiff_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let tiff_row_bytes: usize = tiff_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let chunk_row_bits = (u64::from(tiff_chunk_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let chunk_row_bytes: usize = chunk_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let data_row_bits = (u64::from(data_dimensions.0) * u64::from(tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let data_row_bytes: usize = data_row_bits.div_ceil(8).try_into()?;
|
||||
|
||||
let chunk_col_stride: usize = data_row_bits
|
||||
.div_ceil(8)
|
||||
.checked_mul(u64::from(tiff_chunk_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let plane_stride: usize = data_row_bits
|
||||
.div_ceil(8)
|
||||
.checked_mul(u64::from(data_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let minimum_row_stride = data_row_bytes;
|
||||
|
||||
let chunks_across: u32 = data_dimensions.0.div_ceil(tiff_chunk_dimensions.0);
|
||||
let chunks_per_plane = (self.chunk_offsets.len() as u32) / u32::from(strips_per_pixel);
|
||||
|
||||
// We would not get an offset in byte units, sorry, no bit interleaving in the output.
|
||||
if chunks_across > 1 && chunk_row_bits % 8 != 0 {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::MisalignedTileBoundaries,
|
||||
));
|
||||
}
|
||||
|
||||
// Only this color type interprets the tag, which is defined with a default of (2, 2)
|
||||
if matches!(color, ColorType::YCbCr(_)) && self.chroma_subsampling != (1, 1) {
|
||||
// The JPEG library does upsampling for us and defines its buffers correctly
|
||||
// (presumably). All other compression schemes are not supported..
|
||||
//
|
||||
// NOTE: as explained in <fa225e820b96bef35f01bf4685654beeb4a8df0c> we may be better
|
||||
// off supporting this tag by consistently upsampling, not by adjusting the buffer
|
||||
// size. At least as a default this makes more sense and is much more permissive in
|
||||
// case the compression stream disagrees with the tags (we would not have enough / or
|
||||
// the wrong buffer layout if we only asked for subsampled planes in a planar layout).
|
||||
if !matches!(self.compression_method, CompressionMethod::ModernJPEG) {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::ChromaSubsampling,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ReadoutLayout {
|
||||
planar_config: self.planar_config,
|
||||
color,
|
||||
minimum_row_stride,
|
||||
sample_format: self.sample_format,
|
||||
row_stride: data_row_bytes,
|
||||
chunk_row_stride: chunk_row_bytes,
|
||||
chunk_col_stride,
|
||||
plane_stride,
|
||||
tiff_bits_per_sample,
|
||||
tiff_samples,
|
||||
tiff_chunk_dimensions,
|
||||
tiff_row_bytes,
|
||||
chunks_across,
|
||||
chunks_per_plane,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn chunk_data_dimensions(&self, chunk_index: u32) -> TiffResult<(u32, u32)> {
|
||||
let dims = self.chunk_dimensions()?;
|
||||
|
||||
match self.chunk_type {
|
||||
ChunkType::Strip => {
|
||||
let strip_attrs = self.strip_decoder.as_ref().unwrap();
|
||||
let strips_per_band =
|
||||
self.height.saturating_sub(1) / strip_attrs.rows_per_strip + 1;
|
||||
let rows_per_strip = dims.1;
|
||||
let strips_per_band = self.height.div_ceil(rows_per_strip);
|
||||
|
||||
let strip_height_without_padding = (chunk_index % strips_per_band)
|
||||
.checked_mul(dims.1)
|
||||
.and_then(|x| self.height.checked_sub(x))
|
||||
@@ -535,7 +807,7 @@ impl Image {
|
||||
&self,
|
||||
reader: &mut ValueReader<impl Read>,
|
||||
buf: &mut [u8],
|
||||
output_row_stride: usize,
|
||||
layout: &ReadoutLayout,
|
||||
chunk_index: u32,
|
||||
) -> TiffResult<()> {
|
||||
let ValueReader {
|
||||
@@ -545,10 +817,10 @@ impl Image {
|
||||
} = reader;
|
||||
|
||||
let byte_order = reader.byte_order;
|
||||
let reader = reader.inner();
|
||||
|
||||
// Validate that the color type is supported.
|
||||
let color_type = self.colortype()?;
|
||||
let color_type = layout.color;
|
||||
|
||||
match color_type {
|
||||
ColorType::RGB(n)
|
||||
| ColorType::RGBA(n)
|
||||
@@ -610,6 +882,7 @@ impl Image {
|
||||
.ok_or(TiffError::FormatError(
|
||||
TiffFormatError::InconsistentSizesEncountered,
|
||||
))?;
|
||||
|
||||
if *compressed_bytes > limits.intermediate_buffer_size as u64 {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
}
|
||||
@@ -617,37 +890,43 @@ impl Image {
|
||||
let compression_method = self.compression_method;
|
||||
let photometric_interpretation = self.photometric_interpretation;
|
||||
let predictor = self.predictor;
|
||||
let samples = self.samples_per_pixel();
|
||||
|
||||
let samples = layout.tiff_samples;
|
||||
let data_samples = layout.samples_per_out_texel();
|
||||
|
||||
// We have two dimensions: the 2d rectangle of encoded data and the 2d rectangle this
|
||||
// takes up in the output. Each has an associated count of bits per pixel. The first
|
||||
// dimension, i.e. a ''row'', is the number of pixels that are encoded with bit packing
|
||||
// while the second is the byte-padded array of each so encoded slices.
|
||||
//
|
||||
// During decoding we map the relevant bits from one to the other.
|
||||
let chunk_dims = self.chunk_dimensions()?;
|
||||
let data_dims = self.chunk_data_dimensions(chunk_index)?;
|
||||
|
||||
let chunk_row_bits = (u64::from(chunk_dims.0) * u64::from(self.bits_per_sample))
|
||||
.checked_mul(samples as u64)
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let chunk_row_bytes: usize = ((chunk_row_bits + 7) / 8).try_into()?;
|
||||
|
||||
let data_row_bits = (u64::from(data_dims.0) * u64::from(self.bits_per_sample))
|
||||
.checked_mul(samples as u64)
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
let data_row_bytes: usize = ((data_row_bits + 7) / 8).try_into()?;
|
||||
let chunk_row_bytes: usize = layout.tiff_row_bytes;
|
||||
let data_row_bytes: usize = layout.chunk_row_bytes(data_dims.0)?;
|
||||
|
||||
// TODO: Should these return errors instead?
|
||||
assert!(output_row_stride >= data_row_bytes);
|
||||
assert!(buf.len() >= output_row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
|
||||
assert!(layout.minimum_row_stride >= data_row_bytes);
|
||||
assert!(buf.len() >= layout.row_stride * (data_dims.1 as usize - 1) + data_row_bytes);
|
||||
|
||||
let is_all_bits = samples == data_samples;
|
||||
let is_output_chunk_rows = layout.row_stride == chunk_row_bytes;
|
||||
|
||||
let mut reader = Self::create_reader(
|
||||
reader,
|
||||
reader.inner(),
|
||||
compression_method,
|
||||
*compressed_bytes,
|
||||
self.jpeg_tables.as_deref().map(|a| &**a),
|
||||
chunk_dims,
|
||||
)?;
|
||||
|
||||
if output_row_stride == chunk_row_bytes as usize {
|
||||
if is_output_chunk_rows && is_all_bits {
|
||||
// Here we can read directly into the output buffer itself.
|
||||
let tile = &mut buf[..chunk_row_bytes * data_dims.1 as usize];
|
||||
reader.read_exact(tile)?;
|
||||
|
||||
for row in tile.chunks_mut(chunk_row_bytes as usize) {
|
||||
for row in tile.chunks_mut(chunk_row_bytes) {
|
||||
super::fix_endianness_and_predict(
|
||||
row,
|
||||
color_type.bit_depth(),
|
||||
@@ -656,14 +935,15 @@ impl Image {
|
||||
predictor,
|
||||
);
|
||||
}
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(tile, color_type, self.sample_format);
|
||||
super::invert_colors(tile, color_type, self.sample_format)?;
|
||||
}
|
||||
} else if chunk_row_bytes > data_row_bytes && self.predictor == Predictor::FloatingPoint {
|
||||
// The floating point predictor shuffles the padding bytes into the encoded output, so
|
||||
// this case is handled specially when needed.
|
||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||
for row in buf.chunks_mut(output_row_stride).take(data_dims.1 as usize) {
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
reader.read_exact(&mut encoded)?;
|
||||
|
||||
let row = &mut row[..data_row_bytes];
|
||||
@@ -674,18 +954,19 @@ impl Image {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format);
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (_i, row) in buf
|
||||
.chunks_mut(output_row_stride)
|
||||
.take(data_dims.1 as usize)
|
||||
.enumerate()
|
||||
{
|
||||
} else if is_all_bits {
|
||||
// We read row-by-row but each row fits in its output buffer.
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
let row = &mut row[..data_row_bytes];
|
||||
reader.read_exact(row)?;
|
||||
let used = data_row_bytes.min(chunk_row_bytes);
|
||||
|
||||
// Two ways how we get here: we have more bytes in our chunk data than in the image
|
||||
// we are to read. Then we need to skip the rest of the data. Or we have a bigger
|
||||
// row stride than the chunk contains data, then we need to fill only the front.
|
||||
reader.read_exact(&mut row[..used])?;
|
||||
// Skip horizontal padding
|
||||
if chunk_row_bytes > data_row_bytes {
|
||||
let len = u64::try_from(chunk_row_bytes - data_row_bytes)?;
|
||||
@@ -699,12 +980,183 @@ impl Image {
|
||||
byte_order,
|
||||
predictor,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format);
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The encoded data potentially takes up more space than the output data so we must be
|
||||
// prepared to discard some of it. That decision is bit-by-bit.
|
||||
let bits_per_pixel = u32::from(self.bits_per_sample) * u32::from(self.samples);
|
||||
// Assumes the photometric samples are always the start.. This is slightly problematic.
|
||||
// To expand spport we should instead have different methods of transforming the read
|
||||
// buffer data, not only the `compact_photometric_bytes` method below and then choose
|
||||
// from the right one with supplied parameters. Then we can also bit-for-bit copy with
|
||||
// a selection for better performance.
|
||||
let photometric_bit_end = u32::from(self.bits_per_sample) * data_samples as u32;
|
||||
|
||||
debug_assert!(bits_per_pixel >= photometric_bit_end);
|
||||
|
||||
if bits_per_pixel % 8 != 0 || photometric_bit_end % 8 != 0 {
|
||||
return Err(TiffError::UnsupportedError(
|
||||
TiffUnsupportedError::InterpretationWithBits(
|
||||
self.photometric_interpretation,
|
||||
vec![self.bits_per_sample; self.samples as usize],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let photo_range = photometric_bit_end / 8..bits_per_pixel / 8;
|
||||
let mut encoded = vec![0u8; chunk_row_bytes];
|
||||
for row in buf.chunks_mut(layout.row_stride).take(data_dims.1 as usize) {
|
||||
reader.read_exact(&mut encoded)?;
|
||||
|
||||
Self::compact_photometric_bytes(&mut encoded, row, &photo_range);
|
||||
|
||||
super::fix_endianness_and_predict(
|
||||
row,
|
||||
color_type.bit_depth(),
|
||||
samples,
|
||||
byte_order,
|
||||
predictor,
|
||||
);
|
||||
|
||||
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
|
||||
super::invert_colors(row, color_type, self.sample_format)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Turn a contiguous buffer of a whole number of raw sample arrays into a whole number of
|
||||
/// photometric sample arrays by removing the extra samples in-between.
|
||||
fn compact_photometric_bytes(
|
||||
raw: &mut [u8],
|
||||
row: &mut [u8],
|
||||
photo_range: &std::ops::Range<u32>,
|
||||
) {
|
||||
raw.chunks_exact_mut(photo_range.end as usize)
|
||||
.zip(row.chunks_exact_mut(photo_range.start as usize))
|
||||
.for_each(|(src, dst)| {
|
||||
dst.copy_from_slice(&src[..photo_range.start as usize]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadoutLayout {
|
||||
pub(crate) fn samples_per_out_texel(&self) -> u16 {
|
||||
match self.planar_config {
|
||||
PlanarConfiguration::Chunky => self.color.num_samples(),
|
||||
PlanarConfiguration::Planar => 1,
|
||||
}
|
||||
}
|
||||
|
||||
// For a concrete chunk, which may be a partial border chunk, the byte length of one row of its
|
||||
// pixel data.
|
||||
pub(crate) fn chunk_row_bytes(&self, width: u32) -> TiffResult<usize> {
|
||||
let data_samples = self.samples_per_out_texel();
|
||||
let data_row_bits = (u64::from(width) * u64::from(self.tiff_bits_per_sample))
|
||||
.checked_mul(u64::from(data_samples))
|
||||
.ok_or(TiffError::LimitsExceeded)?;
|
||||
Ok(data_row_bits.div_ceil(8).try_into()?)
|
||||
}
|
||||
|
||||
pub(crate) fn set_row_stride(&mut self, row_stride: usize) -> Result<(), TiffError> {
|
||||
if row_stride < self.minimum_row_stride {
|
||||
return Err(TiffError::UsageError(
|
||||
UsageError::InsufficientOutputRowStride {
|
||||
needed: self.minimum_row_stride,
|
||||
requested: row_stride,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let data_row_bytes = u64::try_from(row_stride)?;
|
||||
|
||||
let chunk_col_stride = data_row_bytes
|
||||
.checked_mul(u64::from(self.tiff_chunk_dimensions.1))
|
||||
.ok_or(TiffError::LimitsExceeded)?
|
||||
.try_into()?;
|
||||
|
||||
let height = self.plane_stride.checked_div(self.row_stride);
|
||||
|
||||
let plane_stride = height
|
||||
.and_then(|h| data_row_bytes.checked_mul(h as u64))
|
||||
// If height was zero, or the previous stride was zero, there are no bytes in a plane
|
||||
.unwrap_or(0)
|
||||
.try_into()?;
|
||||
|
||||
self.row_stride = row_stride;
|
||||
self.chunk_col_stride = chunk_col_stride;
|
||||
self.plane_stride = plane_stride;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reduce this down to the layout of the output planes.
|
||||
pub(crate) fn to_plane_layout(&self) -> Result<PlaneLayout, TiffError> {
|
||||
let num_planes = self.color.num_samples() / self.samples_per_out_texel();
|
||||
|
||||
// Using the standard range iterator as checked_add on steroids.
|
||||
//
|
||||
// Note: for supporting subsampling, adjust as required.
|
||||
let mut offset = (0..=usize::MAX).step_by(self.plane_stride);
|
||||
|
||||
let plane_offsets = offset.by_ref().take(usize::from(num_planes)).collect();
|
||||
|
||||
// Get the past-the-end of the last plane.
|
||||
//
|
||||
// This also verifies the `take` above was not short.
|
||||
let Some(total_bytes) = offset.next() else {
|
||||
return Err(TiffError::LimitsExceeded);
|
||||
};
|
||||
|
||||
Ok(PlaneLayout {
|
||||
plane_offsets,
|
||||
total_bytes,
|
||||
readout: self.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ReadoutLayout` with pre-calculated plane information.
|
||||
pub(crate) struct PlaneLayout {
|
||||
/// The underlying readout layout.
|
||||
pub readout: ReadoutLayout,
|
||||
/// Buffer offset from one plane of output to the next.
|
||||
pub plane_offsets: Vec<usize>,
|
||||
/// Total number of bytes for all planes in given order.
|
||||
pub total_bytes: usize,
|
||||
}
|
||||
|
||||
impl PlaneLayout {
|
||||
/// Return the number of planes to extract into the provided buffer.
|
||||
pub(crate) fn used_planes(&self, buffer: &[impl Sized]) -> TiffResult<u16> {
|
||||
self.readout.assert_min_layout(buffer)?;
|
||||
let buffer_len = core::mem::size_of_val(buffer);
|
||||
|
||||
// Note: with differently sized planes this is dependent on the plane.
|
||||
let last_plane_start = buffer_len.checked_sub(self.readout.plane_stride);
|
||||
|
||||
// Find how many planes fit into the output buffer.
|
||||
let used_plane_offsets = self
|
||||
.plane_offsets
|
||||
.iter()
|
||||
.enumerate()
|
||||
// Find the first plane that would not fit completely at its offset.
|
||||
.skip_while(|(_, &offset)| last_plane_start >= Some(offset))
|
||||
.nth(0)
|
||||
// If all planes fit, use all of them.
|
||||
.map_or(self.plane_offsets.len(), |(idx, _)| idx);
|
||||
|
||||
debug_assert!(
|
||||
used_plane_offsets <= usize::from(u16::MAX),
|
||||
"Planes limited by number of samples, which is encoded as u16"
|
||||
);
|
||||
|
||||
Ok(used_plane_offsets as u16)
|
||||
}
|
||||
}
|
||||
+729
-215
File diff suppressed because it is too large.
Load diff
+112
-36
@@ -1,15 +1,7 @@
|
||||
//! All IO functionality needed for TIFF decoding
|
||||
|
||||
use std::io::{self, BufRead, BufReader, Read, Seek, Take};
|
||||
|
||||
/// Byte order of the TIFF file.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ByteOrder {
|
||||
/// little endian byte order
|
||||
LittleEndian,
|
||||
/// big endian byte order
|
||||
BigEndian,
|
||||
}
|
||||
pub use crate::tags::ByteOrder;
|
||||
|
||||
/// Reader that is aware of the byte order.
|
||||
#[derive(Debug)]
|
||||
@@ -46,17 +38,6 @@ impl<R: Read> EndianReader<R> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads an i8
|
||||
#[inline(always)]
|
||||
pub fn read_i8(&mut self) -> Result<i8, io::Error> {
|
||||
let mut n = [0u8; 1];
|
||||
self.reader.read_exact(&mut n)?;
|
||||
Ok(match self.byte_order {
|
||||
ByteOrder::LittleEndian => i8::from_le_bytes(n),
|
||||
ByteOrder::BigEndian => i8::from_be_bytes(n),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads an i16
|
||||
#[inline(always)]
|
||||
pub fn read_i16(&mut self) -> Result<i16, io::Error> {
|
||||
@@ -135,20 +116,17 @@ impl<R: Read> EndianReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// # READERS
|
||||
///
|
||||
|
||||
///
|
||||
/// ## Deflate Reader
|
||||
///
|
||||
//
|
||||
// # READERS
|
||||
//
|
||||
|
||||
/// Type alias for the deflate Reader
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateReader<R> = flate2::read::ZlibDecoder<R>;
|
||||
|
||||
///
|
||||
/// ## LZW Reader
|
||||
///
|
||||
//
|
||||
// ## LZW Reader
|
||||
//
|
||||
|
||||
/// Reader that decompresses LZW streams
|
||||
#[cfg(feature = "lzw")]
|
||||
@@ -190,9 +168,6 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
Ok(weezl::LzwStatus::NoProgress) => {
|
||||
assert_eq!(result.consumed_in, 0);
|
||||
assert_eq!(result.consumed_out, 0);
|
||||
assert!(self.reader.buffer().is_empty());
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"no lzw end code found",
|
||||
@@ -207,10 +182,11 @@ impl<R: Read> Read for LZWReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## PackBits Reader
|
||||
///
|
||||
//
|
||||
// ## PackBits Reader
|
||||
//
|
||||
|
||||
/// Internal state machine for the PackBitsReader.
|
||||
enum PackBitsReaderState {
|
||||
Header,
|
||||
Literal,
|
||||
@@ -278,6 +254,106 @@ impl<R: Read> Read for PackBitsReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
pub struct Group4Reader<R: Read> {
|
||||
decoder: fax34::decoder::Group4Decoder<io::Bytes<io::BufReader<io::Take<R>>>>,
|
||||
line_buf: io::Cursor<Vec<u8>>,
|
||||
height: u16,
|
||||
width: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Group4Reader<R> {
|
||||
pub fn new(
|
||||
dimensions: (u32, u32),
|
||||
reader: R,
|
||||
compressed_length: u64,
|
||||
) -> crate::TiffResult<Self> {
|
||||
let width = u16::try_from(dimensions.0)?;
|
||||
let height = u16::try_from(dimensions.1)?;
|
||||
|
||||
Ok(Self {
|
||||
decoder: fax34::decoder::Group4Decoder::new(
|
||||
io::BufReader::new(reader.take(compressed_length)).bytes(),
|
||||
width,
|
||||
)?,
|
||||
line_buf: io::Cursor::new(Vec::with_capacity(width.into())),
|
||||
width,
|
||||
height,
|
||||
y: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fax")]
|
||||
impl<R: Read> Read for Group4Reader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
// Either we have not read any line or we are at the end of a line.
|
||||
if self.line_buf.position() as usize == self.line_buf.get_ref().len()
|
||||
&& self.y < self.height
|
||||
{
|
||||
let next = self.decoder.advance().map_err(std::io::Error::other)?;
|
||||
|
||||
match next {
|
||||
fax34::decoder::DecodeStatus::End => (),
|
||||
fax34::decoder::DecodeStatus::Incomplete => {
|
||||
self.y += 1;
|
||||
|
||||
// We known `transitions` yields exactly `self.width` items (per doc).
|
||||
// FIXME: performance. We do not need an individual pixel iterator, filling
|
||||
// memory especially for long runs can be much quicker. The `transitions` are
|
||||
// the positions at which each run-length ends, i.e. the prefix sum of run
|
||||
// lengths. Runs in fax4 start with white.
|
||||
let transitions = fax34::decoder::pels(self.decoder.transition(), self.width);
|
||||
|
||||
let buffer = self.line_buf.get_mut();
|
||||
buffer.resize(usize::from(self.width).div_ceil(8), 0u8);
|
||||
|
||||
let target = &mut buffer[..];
|
||||
|
||||
// Note: it may seem strange to treat black as 0b1 and white as 0b0 despite all
|
||||
// our streams by default decoding as-if PhotometricInterpretation::BlackIsMin.
|
||||
// This is however consistent with libtiff. It seems that fax4's "White"
|
||||
// differs from what libtiff thinks of as "White". For content, a line of data
|
||||
// is in runlength encoding of white-black-white-black always starting with
|
||||
// white. In libtiff, the loop always does both colors in one go and the
|
||||
// structure is:
|
||||
//
|
||||
// ```
|
||||
// for (; runs < erun; runs += 2)
|
||||
// // white run
|
||||
// do { *lp++ = 0L; } while (…)
|
||||
// // black run
|
||||
// do { *lp++ = -1L; } while (…)
|
||||
// ```
|
||||
//
|
||||
// So indeed the Fax4::White run is implemented by filling with zeros.
|
||||
let mut bits = transitions.map(|c| match c {
|
||||
fax34::Color::Black => true,
|
||||
fax34::Color::White => false,
|
||||
});
|
||||
|
||||
// Assemble bits in MSB as per our library representation for buffer.
|
||||
for byte in target {
|
||||
let mut val = 0;
|
||||
|
||||
for (idx, bit) in bits.by_ref().take(8).enumerate() {
|
||||
val |= u8::from(bit) << (7 - idx % 8);
|
||||
}
|
||||
|
||||
*byte = val;
|
||||
}
|
||||
|
||||
self.line_buf.set_position(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.line_buf.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct Directory {
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a directory in an initial state without entries. Note that an empty directory can
|
||||
/// not be encoded in a file, it must contain at least one entry.
|
||||
pub fn empty() -> Self {
|
||||
Directory {
|
||||
entries: BTreeMap::new(),
|
||||
@@ -72,6 +74,12 @@ impl Directory {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Check if there are any entries in this directory. Note that an empty directory can not be
|
||||
/// encoded in the file, it must contain at least one entry.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Get the pointer to the next IFD, if it was defined.
|
||||
pub fn next(&self) -> Option<IfdPointer> {
|
||||
self.next_ifd.map(|n| IfdPointer(n.get()))
|
||||
@@ -96,6 +104,28 @@ impl Directory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encoded_len<K: crate::encoder::TiffKind>(&self) -> u64 {
|
||||
let (len_count, len_offset);
|
||||
|
||||
match std::mem::size_of::<K::OffsetType>() {
|
||||
offset @ 4 => {
|
||||
// non-BigTiff uses a `u16` here.
|
||||
len_count = 2;
|
||||
len_offset = offset as u64;
|
||||
}
|
||||
offset => {
|
||||
let offset = offset as u64;
|
||||
// assuming BigTIFF
|
||||
len_count = offset;
|
||||
len_offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
// Each entry is 12 bytes in classic TIFF, 20 bytes in BigTIFF.
|
||||
let entry_bytes = 4 + 2 * len_offset;
|
||||
len_count + entry_bytes * self.entries.len() as u64 + len_offset
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Directory {
|
||||
@@ -110,6 +140,14 @@ impl fmt::Debug for Directory {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::iter::FromIterator<(Tag, Entry)> for Directory {
|
||||
fn from_iter<T: IntoIterator<Item = (Tag, Entry)>>(iter: T) -> Self {
|
||||
let mut dir = Directory::empty();
|
||||
dir.extend(iter);
|
||||
dir
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Directory;
|
||||
|
||||
@@ -11,21 +11,17 @@ pub struct Deflate {
|
||||
/// It allows trading compression ratio for compression speed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub enum DeflateLevel {
|
||||
/// The fastest possible compression mode.
|
||||
Fast = 1,
|
||||
/// The conserative choice between speed and ratio.
|
||||
#[default]
|
||||
Balanced = 6,
|
||||
/// The best compression available with Deflate.
|
||||
Best = 9,
|
||||
}
|
||||
|
||||
impl Default for DeflateLevel {
|
||||
fn default() -> Self {
|
||||
DeflateLevel::Balanced
|
||||
}
|
||||
}
|
||||
|
||||
impl Deflate {
|
||||
/// Create a new deflate compressor with a specific level of compression.
|
||||
pub fn with_level(level: DeflateLevel) -> Self {
|
||||
|
||||
@@ -14,7 +14,7 @@ impl Compression for Uncompressed {
|
||||
|
||||
impl CompressionAlgorithm for Uncompressed {
|
||||
fn write_to<W: Write>(&mut self, writer: &mut W, bytes: &[u8]) -> Result<u64, io::Error> {
|
||||
writer.write(bytes).map(|byte_count| byte_count as u64)
|
||||
writer.write_all(bytes).map(|()| bytes.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+199
-23
@@ -11,7 +11,10 @@ use std::{
|
||||
use crate::{
|
||||
decoder::ifd::Entry,
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{CompressionMethod, IfdPointer, ResolutionUnit, SampleFormat, Tag, Type},
|
||||
tags::{
|
||||
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
|
||||
SampleFormat, Tag, Type,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
@@ -37,8 +40,9 @@ pub type Predictor = crate::tags::Predictor;
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateLevel = compression::DeflateLevel;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub enum Compression {
|
||||
#[default]
|
||||
Uncompressed,
|
||||
#[cfg(feature = "lzw")]
|
||||
Lzw,
|
||||
@@ -47,12 +51,6 @@ pub enum Compression {
|
||||
Packbits,
|
||||
}
|
||||
|
||||
impl Default for Compression {
|
||||
fn default() -> Self {
|
||||
Self::Uncompressed
|
||||
}
|
||||
}
|
||||
|
||||
impl Compression {
|
||||
fn tag(&self) -> CompressionMethod {
|
||||
match self {
|
||||
@@ -79,10 +77,10 @@ impl Compression {
|
||||
|
||||
/// Encoder for Tiff and BigTiff files.
|
||||
///
|
||||
/// With this type you can get a `DirectoryEncoder` or a `ImageEncoder`
|
||||
/// With this type you can get a [`DirectoryEncoder`] or a [`ImageEncoder`]
|
||||
/// to encode Tiff/BigTiff ifd directories with images.
|
||||
///
|
||||
/// See `DirectoryEncoder` and `ImageEncoder`.
|
||||
/// See [`DirectoryEncoder`] and [`ImageEncoder`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -176,7 +174,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
|
||||
}
|
||||
|
||||
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
|
||||
/// Create a [`DirectoryEncoder`] to encode an ifd (directory).
|
||||
///
|
||||
/// The caller is responsible for ensuring that the directory is a valid image in the main TIFF
|
||||
/// IFD sequence. To encode additional directories that are not linked into the sequence, use
|
||||
@@ -185,7 +183,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)
|
||||
}
|
||||
|
||||
/// Create a [`DirectoryEncoder`] to encode an ifd directory.
|
||||
/// Create a [`DirectoryEncoder`] to encode a non-image directory.
|
||||
///
|
||||
/// The directory is not linked into the sequence of directories. For instance, encode Exif
|
||||
/// directories or SubIfd directories with this method.
|
||||
@@ -198,7 +196,7 @@ impl<W: Write + Seek, K: TiffKind> TiffEncoder<W, K> {
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> TiffResult<ImageEncoder<W, C, K>> {
|
||||
) -> TiffResult<ImageEncoder<'_, W, C, K>> {
|
||||
let encoder = Self::chain_directory(&mut self.writer, &mut self.last_ifd_chain)?;
|
||||
ImageEncoder::new(encoder, width, height, self.compression, self.predictor)
|
||||
}
|
||||
@@ -249,6 +247,12 @@ pub struct DirectoryEncoder<'a, W: 'a + Write + Seek, K: TiffKind> {
|
||||
}
|
||||
|
||||
/// The offset of an encoded directory in the file.
|
||||
///
|
||||
/// Both the `offset` and `pointer` field are a complete description of the start offset of the
|
||||
/// dictionary which is used to point to it. The struct also describes the extent covered by the
|
||||
/// encoding of the directory. The constructors [`new`][DirectoryOffset::new] allow the checked
|
||||
/// conversion. Note that even though the offsets are public do not expect their information to
|
||||
/// propagate when modified.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DirectoryOffset<K: TiffKind> {
|
||||
/// The start of the directory as a Tiff value.
|
||||
@@ -260,7 +264,9 @@ pub struct DirectoryOffset<K: TiffKind> {
|
||||
pub offset: K::OffsetType,
|
||||
/// The start of the directory as a pure offset.
|
||||
pub pointer: IfdPointer,
|
||||
/// The offset of its sequence link field, in our private representation.
|
||||
/// The offset of its sequence link field, in our private representation. Before exposing this
|
||||
/// make sure that we don't want any invariants (e.g. pointer being smaller than this). The
|
||||
/// type should be fine.
|
||||
ifd_chain: NonZeroU64,
|
||||
/// The kind of Tiff file the offset is for.
|
||||
kind: PhantomData<K>,
|
||||
@@ -296,7 +302,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
}
|
||||
|
||||
let entry = Self::write_value(
|
||||
&mut self.writer,
|
||||
self.writer,
|
||||
&DirectoryEntry {
|
||||
data_type: <T>::FIELD_TYPE,
|
||||
count: value.count().try_into()?,
|
||||
@@ -318,6 +324,17 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
/// Insert previously written key-value entries.
|
||||
///
|
||||
/// It is the caller's responsibility to ensure that the data referred to by the entries is
|
||||
/// actually (or will be) written to the file. Note that small values are necessarily inline in
|
||||
/// the entry, the size limit depends on the Tiff kind. So do not confuse a directory from a
|
||||
/// BigTiff in a standard tiff or the other way around.
|
||||
pub fn extend_from(&mut self, dir: &Directory) {
|
||||
let entries = dir.iter().map(|(tag, val)| (tag, val.clone()));
|
||||
self.directory.extend(entries);
|
||||
}
|
||||
|
||||
/// Define the parent directory.
|
||||
///
|
||||
/// Each directory has an offset based link to its successor, forming a linked list in the
|
||||
@@ -334,6 +351,11 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the IFD to the file and return the offset it is written to.
|
||||
///
|
||||
/// The offset can be used as the value, or as part of a list of values, in the entry of
|
||||
/// another directory. If you're constructing the entry directly you'll want to use an
|
||||
/// appropriate type variant for this such as [`Type::IFD`].
|
||||
pub fn finish_with_offsets(mut self) -> TiffResult<DirectoryOffset<K>> {
|
||||
self.finish_internal()
|
||||
}
|
||||
@@ -370,14 +392,14 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
|
||||
if bytes.len() > in_entry_bytes {
|
||||
let offset = writer.offset();
|
||||
writer.write_bytes(&bytes)?;
|
||||
writer.write_bytes(bytes)?;
|
||||
|
||||
let offset = K::convert_offset(offset)?;
|
||||
offset_bytes[..offset.bytes()].copy_from_slice(&offset.data());
|
||||
} else {
|
||||
// Note: we have indicated our native byte order in the header, hence this
|
||||
// corresponds to our byte order no matter the value type.
|
||||
offset_bytes[..bytes.len()].copy_from_slice(&bytes);
|
||||
offset_bytes[..bytes.len()].copy_from_slice(bytes);
|
||||
}
|
||||
|
||||
// Undoing some hidden type. Offset is either u32 or u64. Due to the trait API being public
|
||||
@@ -487,6 +509,7 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> {
|
||||
row_samples: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
extra_samples: Vec<u16>,
|
||||
rows_per_strip: u64,
|
||||
strip_offsets: Vec<K::OffsetType>,
|
||||
strip_byte_count: Vec<K::OffsetType>,
|
||||
@@ -532,22 +555,25 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
let rows_per_strip = {
|
||||
match compression.tag() {
|
||||
CompressionMethod::PackBits => 1, // Each row must be packed separately. Do not compress across row boundaries
|
||||
_ => (1_000_000 + row_bytes - 1) / row_bytes,
|
||||
_ => 1_000_000_u64.div_ceil(row_bytes),
|
||||
}
|
||||
};
|
||||
|
||||
let strip_count = (u64::from(height) + rows_per_strip - 1) / rows_per_strip;
|
||||
let strip_count = u64::from(height).div_ceil(rows_per_strip);
|
||||
|
||||
encoder.write_tag(Tag::ImageWidth, width)?;
|
||||
encoder.write_tag(Tag::ImageLength, height)?;
|
||||
encoder.write_tag(Tag::Compression, compression.tag().to_u16())?;
|
||||
encoder.write_tag(Tag::Predictor, predictor.to_u16())?;
|
||||
|
||||
encoder.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
|
||||
let sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
|
||||
encoder.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
||||
encoder.write_tag(Tag::PhotometricInterpretation, <T>::TIFF_VALUE.to_u16())?;
|
||||
|
||||
if matches!(<T>::TIFF_VALUE, PhotometricInterpretation::YCbCr) {
|
||||
// The default for this tag is 2,2 for subsampling but we do not support such a
|
||||
// transformation. Instead all samples must be provided.
|
||||
encoder.write_tag(Tag::ChromaSubsampling, &[1u16, 1u16][..])?;
|
||||
}
|
||||
|
||||
encoder.write_tag(Tag::RowsPerStrip, u32::try_from(rows_per_strip)?)?;
|
||||
|
||||
encoder.write_tag(
|
||||
@@ -564,6 +590,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
strip_idx: 0,
|
||||
row_samples,
|
||||
rows_per_strip,
|
||||
extra_samples: vec![],
|
||||
width,
|
||||
height,
|
||||
strip_offsets: Vec::new(),
|
||||
@@ -575,6 +602,27 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extra_samples(&mut self, extra: &[ExtraSamples]) -> Result<(), TiffError> {
|
||||
if self.strip_idx != 0 {
|
||||
return Err(TiffError::UsageError(
|
||||
UsageError::ReconfiguredAfterImageWrite,
|
||||
));
|
||||
}
|
||||
|
||||
let samples = self.extra_samples.len() + extra.len() + <T>::BITS_PER_SAMPLE.len();
|
||||
let row_samples = u64::from(self.width) * u64::try_from(samples)?;
|
||||
|
||||
self.extra_samples
|
||||
.extend(extra.iter().map(ExtraSamples::to_u16));
|
||||
|
||||
self.encoder
|
||||
.write_tag(Tag::ExtraSamples, &self.extra_samples[..])?;
|
||||
|
||||
self.row_samples = row_samples;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Number of samples the next strip should have.
|
||||
pub fn next_strip_sample_count(&self) -> u64 {
|
||||
if self.strip_idx >= self.strip_count {
|
||||
@@ -705,13 +753,46 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
self.encoder.write_tag(Tag::RowsPerStrip, value)?;
|
||||
|
||||
let value: u64 = value as u64;
|
||||
self.strip_count = (self.height as u64 + value - 1) / value;
|
||||
self.strip_count = (self.height as u64).div_ceil(value);
|
||||
self.rows_per_strip = value;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_internal(&mut self) -> TiffResult<DirectoryOffset<K>> {
|
||||
if self.extra_samples.is_empty() {
|
||||
self.encoder
|
||||
.write_tag(Tag::BitsPerSample, <T>::BITS_PER_SAMPLE)?;
|
||||
} else {
|
||||
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
|
||||
let replicated =
|
||||
core::iter::repeat(<T>::BITS_PER_SAMPLE[0]).take(self.extra_samples.len());
|
||||
sample_format.extend(replicated);
|
||||
|
||||
self.encoder
|
||||
.write_tag(Tag::BitsPerSample, &sample_format[..])?;
|
||||
|
||||
self.encoder.write_tag(
|
||||
Tag::SamplesPerPixel,
|
||||
u16::try_from(<T>::BITS_PER_SAMPLE.len() + self.extra_samples.len())?,
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut sample_format: Vec<_> = <T>::SAMPLE_FORMAT.iter().map(|s| s.to_u16()).collect();
|
||||
let extra_format = sample_format
|
||||
.first()
|
||||
.copied()
|
||||
// Frankly should not occur, we have no sample format without at least one entry. We
|
||||
// would need an interface upgrade to handle this however. Either decide to support
|
||||
// heterogeneous sample formats or a way to provide fallback that is used for purely
|
||||
// extra samples and not provided via a slice used for samples themselves.
|
||||
.unwrap_or(SampleFormat::Void.to_u16());
|
||||
|
||||
sample_format.extend(core::iter::repeat(extra_format).take(self.extra_samples.len()));
|
||||
|
||||
self.encoder
|
||||
.write_tag(Tag::SampleFormat, &sample_format[..])?;
|
||||
|
||||
self.encoder
|
||||
.write_tag(Tag::StripOffsets, K::convert_slice(&self.strip_offsets))?;
|
||||
self.encoder.write_tag(
|
||||
@@ -791,6 +872,71 @@ pub trait TiffKind {
|
||||
fn convert_slice(slice: &[Self::OffsetType]) -> &Self::OffsetArrayType;
|
||||
}
|
||||
|
||||
impl<K: TiffKind> DirectoryOffset<K> {
|
||||
/// Construct the directory pointer information from its raw offset.
|
||||
///
|
||||
/// Fails in these cases:
|
||||
///
|
||||
/// - if start offset is too large to be represented with the chosen TIFF format, i.e. a value
|
||||
/// larger than [`u32::MAX`] with a standard tiff which is only valid in a BigTiff. This
|
||||
/// returns [`TiffError::IntSizeError`].
|
||||
/// - the extent of the directory would overflow the file length. This returns a
|
||||
/// [`TiffError::UsageError`].
|
||||
///
|
||||
/// The library does *not* validate the contents or offset in any way, it just performs the
|
||||
/// necessary math assuming the data is an accurate representation of existing content.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// If you have any custom way of writing a directory to a file that can not go through the
|
||||
/// usual [`TiffEncoder::extra_directory`] then you can reconstruct the offset information as
|
||||
/// long as you provide all the necessary data to the library. You can then use it as a parent
|
||||
/// directory, i.e. have the library overwrite its `next` field.
|
||||
///
|
||||
/// ```
|
||||
/// use tiff::{
|
||||
/// encoder::{TiffEncoder, DirectoryOffset},
|
||||
/// Directory,
|
||||
/// tags::IfdPointer,
|
||||
/// };
|
||||
///
|
||||
/// # let mut file = std::io::Cursor::new(Vec::new());
|
||||
/// let mut tiff = TiffEncoder::new(&mut file)?;
|
||||
///
|
||||
/// // … some custom data writes, assume we know where a directory was written
|
||||
/// # fn reconstruction_of_your_directory() -> Directory { Directory::from_iter([]) }
|
||||
/// let known_offset = IfdPointer(1024);
|
||||
/// let known_contents: Directory = reconstruction_of_your_directory();
|
||||
///
|
||||
/// let reconstructed = DirectoryOffset::new(known_offset, &known_contents)?;
|
||||
///
|
||||
/// // Now we can use it as if the directory was written by the `TiffEncoder` itself.
|
||||
/// let mut chain = tiff.extra_directory()?;
|
||||
/// chain.set_parent(&reconstructed);
|
||||
/// chain.finish_with_offsets()?;
|
||||
///
|
||||
/// # Ok::<_, tiff::TiffError>(())
|
||||
/// ```
|
||||
pub fn new(pointer: IfdPointer, dir: &Directory) -> TiffResult<Self> {
|
||||
let offset = K::convert_offset(pointer.0)?;
|
||||
let encoded = dir.encoded_len::<K>();
|
||||
let offset_field_offset = encoded - mem::size_of_val(&offset) as u64;
|
||||
|
||||
let ifd_chain = pointer
|
||||
.0
|
||||
.checked_add(offset_field_offset)
|
||||
.and_then(NonZeroU64::new)
|
||||
.ok_or(TiffError::UsageError(UsageError::ZeroIfdPointer))?;
|
||||
|
||||
Ok(DirectoryOffset {
|
||||
pointer,
|
||||
offset,
|
||||
ifd_chain,
|
||||
kind: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a standard Tiff file.
|
||||
pub struct TiffKindStandard;
|
||||
|
||||
@@ -859,3 +1005,33 @@ impl TiffKind for TiffKindBig {
|
||||
slice
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_offset_new_equivalent_to_writing() {
|
||||
type K = TiffKindStandard;
|
||||
|
||||
let dir = Directory::from_iter(vec![
|
||||
(
|
||||
Tag::ImageWidth,
|
||||
Entry::new(Type::LONG, 1, 100u32.to_ne_bytes()),
|
||||
),
|
||||
(
|
||||
Tag::ImageLength,
|
||||
Entry::new(Type::LONG, 1, 200u32.to_ne_bytes()),
|
||||
),
|
||||
]);
|
||||
|
||||
let data = std::io::Cursor::new(vec![]);
|
||||
let mut file = TiffEncoder::new(data).unwrap();
|
||||
|
||||
let mut as_dir_encoder = file.extra_directory().unwrap();
|
||||
as_dir_encoder.extend_from(&dir);
|
||||
let dir_extent = as_dir_encoder.finish_with_offsets().unwrap();
|
||||
|
||||
// Check we can recreate the extent had we written ourselves.
|
||||
let synth = DirectoryOffset::<K>::new(dir_extent.pointer, &dir).unwrap();
|
||||
|
||||
assert_eq!(synth.pointer, dir_extent.pointer);
|
||||
assert_eq!(synth.offset, dir_extent.offset);
|
||||
assert_eq!(synth.ifd_chain, dir_extent.ifd_chain);
|
||||
}
|
||||
+29
-29
@@ -15,7 +15,7 @@ pub trait TiffValue {
|
||||
|
||||
/// Access this value as an contiguous sequence of bytes.
|
||||
/// If their is no trivial representation, allocate it on the heap.
|
||||
fn data(&self) -> Cow<[u8]>;
|
||||
fn data(&self) -> Cow<'_, [u8]>;
|
||||
|
||||
/// Write this value to a TiffWriter.
|
||||
/// While the default implementation will work in all cases, it may require unnecessary allocations.
|
||||
@@ -34,7 +34,7 @@ impl TiffValue for [u8] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(self)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ impl TiffValue for [i8] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ impl TiffValue for [u16] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ impl TiffValue for [i16] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl TiffValue for [u32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ impl TiffValue for [i32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ impl TiffValue for [u64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl TiffValue for [i64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ impl TiffValue for [f32] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
// We write using native endian so this should be safe
|
||||
Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
|
||||
}
|
||||
@@ -152,7 +152,7 @@ impl TiffValue for [f64] {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
// We write using native endian so this should be safe
|
||||
Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
|
||||
}
|
||||
@@ -171,7 +171,7 @@ impl TiffValue for u8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(from_ref(self))
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ impl TiffValue for i8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ impl TiffValue for u16 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,7 @@ impl TiffValue for i16 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ impl TiffValue for u32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ impl TiffValue for i32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -279,7 +279,7 @@ impl TiffValue for u64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -297,7 +297,7 @@ impl TiffValue for i64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ impl TiffValue for f32 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -333,7 +333,7 @@ impl TiffValue for f64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@ impl TiffValue for Ifd {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
|
||||
}
|
||||
}
|
||||
@@ -369,7 +369,7 @@ impl TiffValue for Ifd8 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ impl TiffValue for Rational {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Owned({
|
||||
let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n));
|
||||
let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d));
|
||||
@@ -411,7 +411,7 @@ impl TiffValue for SRational {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Owned({
|
||||
let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n));
|
||||
let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d));
|
||||
@@ -438,7 +438,7 @@ impl TiffValue for str {
|
||||
}
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
Cow::Owned({
|
||||
if self.is_ascii() && !self.bytes().any(|b| b == 0) {
|
||||
let bytes: &[u8] = self.as_bytes();
|
||||
@@ -450,7 +450,7 @@ impl TiffValue for str {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
|
||||
impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
|
||||
const BYTE_LEN: u8 = T::BYTE_LEN;
|
||||
const FIELD_TYPE: Type = T::FIELD_TYPE;
|
||||
|
||||
@@ -462,7 +462,7 @@ impl<'a, T: TiffValue + ?Sized> TiffValue for &'a T {
|
||||
(*self).write(writer)
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
T::data(self)
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@ macro_rules! impl_tiff_value_for_contiguous_sequence {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data(&self) -> Cow<[u8]> {
|
||||
fn data(&self) -> Cow<'_, [u8]> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(Self::BYTE_LEN as usize * self.len());
|
||||
for x in self {
|
||||
buf.extend_from_slice(&x.data());
|
||||
|
||||
@@ -190,3 +190,64 @@ impl<W: Seek> TiffWriter<W> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_on_short_writes() {
|
||||
struct ShortWriter<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: std::io::Write> std::io::Write for ShortWriter<T> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let lower_bound = buf.len().min(1);
|
||||
let lower = buf.len().saturating_sub(1).max(lower_bound);
|
||||
|
||||
self.inner.write(&buf[..lower])
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::io::Seek> std::io::Seek for ShortWriter<T> {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
let mut write = ShortWriter {
|
||||
inner: std::io::Cursor::new(vec![0u8; 0]),
|
||||
};
|
||||
|
||||
let imgdata = vec![42u8; 100 * 100];
|
||||
let mut data = crate::decoder::DecodingResult::U8(vec![]);
|
||||
|
||||
for comp in [
|
||||
super::Compression::Uncompressed,
|
||||
super::Compression::Packbits,
|
||||
] {
|
||||
write.inner.set_position(0);
|
||||
write.inner.get_mut().clear();
|
||||
|
||||
{
|
||||
let mut tiff_writer = super::TiffEncoder::new(&mut write)
|
||||
.unwrap()
|
||||
.with_compression(comp);
|
||||
|
||||
tiff_writer
|
||||
.write_image::<super::Gray8>(100, 100, &imgdata)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
write.inner.set_position(0);
|
||||
let mut tiff_reader = crate::decoder::Decoder::new(&mut write.inner).unwrap();
|
||||
let _layout = tiff_reader.read_image_to_buffer(&mut data).unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&data, crate::decoder::DecodingResult::U8(v) if *v == imgdata),
|
||||
"{data:?} @ {}",
|
||||
data.as_buffer(0).byte_len()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,9 @@ quick_error! {
|
||||
InvalidTypeForTag {
|
||||
display("tag has invalid type")
|
||||
}
|
||||
InvalidCountForTag(tag: Tag, len: usize) {
|
||||
display("tag `{tag:?}` with incorrect number of elements ({len}) encountered")
|
||||
}
|
||||
StripTileTagConflict {
|
||||
display("file should contain either (StripByteCounts and StripOffsets) or (TileByteCounts and TileOffsets), other combination was found")
|
||||
}
|
||||
@@ -156,6 +159,9 @@ quick_error! {
|
||||
UnsupportedInterpretation(interpretation: PhotometricInterpretation) {
|
||||
display("unsupported photometric interpretation \"{interpretation:?}\"")
|
||||
}
|
||||
ChromaSubsampling {
|
||||
display("chroma subsampling of YCbCr color is unsupported")
|
||||
}
|
||||
MisalignedTileBoundaries {
|
||||
display("tile rows are not aligned to byte boundaries")
|
||||
}
|
||||
@@ -175,6 +181,12 @@ quick_error! {
|
||||
InvalidChunkIndex(index: u32) {
|
||||
display("invalid chunk index ({index}) requested")
|
||||
}
|
||||
InvalidPlaneIndex(index: u16) {
|
||||
display("invalid plane index ({index}) requested")
|
||||
}
|
||||
InvalidCodingUnit(index: u32, have: u32) {
|
||||
display("out of bounds coding unit ({index}) requested, only {have} available")
|
||||
}
|
||||
PredictorCompressionMismatch {
|
||||
display("requested predictor is not compatible with the requested compression")
|
||||
}
|
||||
@@ -184,6 +196,18 @@ quick_error! {
|
||||
PredictorUnavailable {
|
||||
display("the requested predictor is not available")
|
||||
}
|
||||
InsufficientOutputBufferSize { needed: usize, provided: usize } {
|
||||
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
|
||||
}
|
||||
InsufficientOutputRowStride { needed: usize, requested: usize } {
|
||||
display("the provided output row stride would alias rows of decoded data, needed {needed} but have {requested}")
|
||||
}
|
||||
ZeroIfdPointer {
|
||||
display("the offset 0 can not point to a valid IFD")
|
||||
}
|
||||
ReconfiguredAfterImageWrite {
|
||||
display("attempted to reconfigure the encoder after image writing has started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -44,6 +44,9 @@ pub enum ColorType {
|
||||
/// Pixel is YCbCr
|
||||
YCbCr(u8),
|
||||
|
||||
/// Pixel is CIE L*a*b* (ICC LAb)
|
||||
Lab(u8),
|
||||
|
||||
/// Pixel has multiple bands/channels
|
||||
Multiband { bit_depth: u8, num_samples: u16 },
|
||||
}
|
||||
@@ -58,7 +61,23 @@ impl ColorType {
|
||||
| ColorType::CMYK(b)
|
||||
| ColorType::CMYKA(b)
|
||||
| ColorType::YCbCr(b)
|
||||
| ColorType::Lab(b)
|
||||
| ColorType::Multiband { bit_depth: b, .. } => b,
|
||||
}
|
||||
}
|
||||
|
||||
fn num_samples(&self) -> u16 {
|
||||
match *self {
|
||||
ColorType::Gray(_) => 1,
|
||||
ColorType::RGB(_) => 3,
|
||||
ColorType::Palette(_) => 1,
|
||||
ColorType::GrayA(_) => 2,
|
||||
ColorType::RGBA(_) => 4,
|
||||
ColorType::CMYK(_) => 4,
|
||||
ColorType::CMYKA(_) => 5,
|
||||
ColorType::YCbCr(_) => 3,
|
||||
ColorType::Lab(_) => 3,
|
||||
ColorType::Multiband { num_samples, .. } => num_samples,
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
-15
@@ -2,7 +2,7 @@ macro_rules! tags {
|
||||
{
|
||||
// Permit arbitrary meta items, which include documentation.
|
||||
$( #[$enum_attr:meta] )*
|
||||
$vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* {
|
||||
$vis:vis enum $name:ident($ty:tt) $(unknown(#[$unknown_meta:meta] $unknown_doc:ident))* {
|
||||
// Each of the `Name = Val,` permitting documentation.
|
||||
$($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
|
||||
}
|
||||
@@ -13,14 +13,14 @@ macro_rules! tags {
|
||||
pub enum $name {
|
||||
$($(#[$ident_attr])* $tag,)*
|
||||
$(
|
||||
#[doc = $unknown_doc]
|
||||
#[$unknown_meta]
|
||||
Unknown($ty),
|
||||
)*
|
||||
}
|
||||
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
const fn __from_inner_type(n: $ty) -> Result<Self, $ty> {
|
||||
match n {
|
||||
$( $val => Ok($name::$tag), )*
|
||||
n => Err(n),
|
||||
@@ -28,10 +28,10 @@ macro_rules! tags {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn __to_inner_type(&self) -> $ty {
|
||||
const fn __to_inner_type(&self) -> $ty {
|
||||
match *self {
|
||||
$( $name::$tag => $val, )*
|
||||
$( $name::Unknown(n) => { $unknown_doc; n }, )*
|
||||
$( $name::Unknown($unknown_doc) => { $unknown_doc }, )*
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,23 +39,28 @@ macro_rules! tags {
|
||||
tags!($name, $ty, $($unknown_doc)*);
|
||||
};
|
||||
// For u16 tags, provide direct inherent primitive conversion methods.
|
||||
($name:tt, u16, $($unknown_doc:literal)*) => {
|
||||
($name:tt, u16, $($unknown_doc:ident)*) => {
|
||||
impl $name {
|
||||
#[inline(always)]
|
||||
pub fn from_u16(val: u16) -> Option<Self> {
|
||||
Self::__from_inner_type(val).ok()
|
||||
pub const fn from_u16(val: u16) -> Option<Self> {
|
||||
match Self::__from_inner_type(val) {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[inline(always)]
|
||||
pub fn from_u16_exhaustive(val: u16) -> Self {
|
||||
$unknown_doc;
|
||||
Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val))
|
||||
pub const fn from_u16_exhaustive($unknown_doc: u16) -> Self {
|
||||
match Self::__from_inner_type($unknown_doc) {
|
||||
Ok(v) => v,
|
||||
Err(_) => $name::Unknown($unknown_doc),
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_u16(&self) -> u16 {
|
||||
pub const fn to_u16(&self) -> u16 {
|
||||
Self::__to_inner_type(self)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +73,10 @@ macro_rules! tags {
|
||||
// Note: These tags appear in the order they are mentioned in the TIFF reference
|
||||
tags! {
|
||||
/// TIFF tags
|
||||
pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
pub enum Tag(u16) unknown(
|
||||
/// A private or extension tag
|
||||
unknown
|
||||
) {
|
||||
// Baseline tags:
|
||||
Artist = 315,
|
||||
// grayscale images PhotometricInterpretation 1 or 3
|
||||
@@ -120,6 +128,11 @@ pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
SMaxSampleValue = 341, // TODO add support
|
||||
// JPEG
|
||||
JPEGTables = 347,
|
||||
// Subsampling
|
||||
#[doc(alias = "YCbCrSubsampling")]
|
||||
ChromaSubsampling = 530, // TODO add support
|
||||
#[doc(alias = "YCbCrPositioning")]
|
||||
ChromaPositioning = 531, // TODO add support
|
||||
// GeoTIFF
|
||||
ModelPixelScaleTag = 33550, // (SoftDesk)
|
||||
ModelTransformationTag = 34264, // (JPL Carto Group)
|
||||
@@ -131,6 +144,8 @@ pub enum Tag(u16) unknown("A private or extension tag") {
|
||||
ExifDirectory = 0x8769,
|
||||
// <https://web.archive.org/web/20131111073619/http://www.exif.org/Exif2-1.PDF>
|
||||
GpsDirectory = 0x8825,
|
||||
// <https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf>
|
||||
IccProfile = 34675,
|
||||
GeoKeyDirectoryTag = 34735, // (SPOT)
|
||||
GeoDoubleParamsTag = 34736, // (SPOT)
|
||||
GeoAsciiParamsTag = 34737, // (SPOT)
|
||||
@@ -189,10 +204,52 @@ pub enum Type(u16) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub(crate) fn byte_len(&self) -> u8 {
|
||||
match *self {
|
||||
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => 1,
|
||||
Type::SHORT | Type::SSHORT => 2,
|
||||
Type::LONG | Type::SLONG | Type::FLOAT | Type::IFD => 4,
|
||||
Type::LONG8
|
||||
| Type::SLONG8
|
||||
| Type::DOUBLE
|
||||
| Type::RATIONAL
|
||||
| Type::SRATIONAL
|
||||
| Type::IFD8 => 8,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn value_bytes(&self, count: u64) -> Result<u64, crate::error::TiffError> {
|
||||
let tag_size = u64::from(self.byte_len());
|
||||
|
||||
match count.checked_mul(tag_size) {
|
||||
Some(n) => Ok(n),
|
||||
None => Err(crate::error::TiffError::LimitsExceeded),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn endian_bytes(self) -> EndianBytes {
|
||||
match self {
|
||||
Type::BYTE | Type::SBYTE | Type::ASCII | Type::UNDEFINED => EndianBytes::One,
|
||||
Type::SHORT | Type::SSHORT => EndianBytes::Two,
|
||||
Type::LONG
|
||||
| Type::SLONG
|
||||
| Type::FLOAT
|
||||
| Type::IFD
|
||||
| Type::RATIONAL
|
||||
| Type::SRATIONAL => EndianBytes::Four,
|
||||
Type::LONG8 | Type::SLONG8 | Type::DOUBLE | Type::IFD8 => EndianBytes::Eight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags! {
|
||||
/// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
|
||||
/// for reference.
|
||||
pub enum CompressionMethod(u16) unknown("A custom compression method") {
|
||||
pub enum CompressionMethod(u16) unknown(
|
||||
/// A custom compression method
|
||||
unknown
|
||||
) {
|
||||
None = 1,
|
||||
Huffman = 2,
|
||||
Fax3 = 3,
|
||||
@@ -220,6 +277,8 @@ pub enum PhotometricInterpretation(u16) {
|
||||
CMYK = 5,
|
||||
YCbCr = 6,
|
||||
CIELab = 8,
|
||||
IccLab = 9,
|
||||
ItuLab = 10,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,10 +313,100 @@ pub enum ResolutionUnit(u16) {
|
||||
}
|
||||
|
||||
tags! {
|
||||
pub enum SampleFormat(u16) unknown("An unknown extension sample format") {
|
||||
pub enum SampleFormat(u16) unknown(
|
||||
/// An unknown extension sample format
|
||||
unknown
|
||||
) {
|
||||
Uint = 1,
|
||||
Int = 2,
|
||||
IEEEFP = 3,
|
||||
Void = 4,
|
||||
}
|
||||
}
|
||||
|
||||
tags! {
|
||||
pub enum ExtraSamples(u16) {
|
||||
/// There is no specified association between the sample and the image.
|
||||
Unspecified = 0,
|
||||
/// The sample is associated alpha, i.e. pre-multiplied color.
|
||||
AssociatedAlpha = 1,
|
||||
/// The sample is unassociated alpha such as a mask. There might be more than one such sample.
|
||||
UnassociatedAlpha = 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte order of the TIFF file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ByteOrder {
|
||||
/// little endian byte order
|
||||
LittleEndian,
|
||||
/// big endian byte order
|
||||
BigEndian,
|
||||
}
|
||||
|
||||
impl ByteOrder {
|
||||
/// Get the byte order representing the running target.
|
||||
///
|
||||
/// The infallibility of this method represents the fact that only little and big endian
|
||||
/// systems are supported by the library. No mixed endian and no other weird stuff. (Note: as
|
||||
/// of Rust 1.90 this is a tautology as Rust itself only has those two kinds).
|
||||
pub const fn native() -> Self {
|
||||
match () {
|
||||
#[cfg(target_endian = "little")]
|
||||
() => ByteOrder::LittleEndian,
|
||||
#[cfg(target_endian = "big")]
|
||||
() => ByteOrder::BigEndian,
|
||||
#[cfg(not(any(target_endian = "big", target_endian = "little")))]
|
||||
() => compile_error!("Unsupported target"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a typed buffer, convert its contents to the specified byte order in-place.
|
||||
///
|
||||
/// The buffer is assumed to represent an array of the given type. If the length of the buffer
|
||||
/// is not divisible into an integer number of values, the behavior for the remaining bytes it
|
||||
/// not specified.
|
||||
pub fn convert(self, ty: Type, buffer: &mut [u8], to: ByteOrder) {
|
||||
self.convert_endian_bytes(ty.endian_bytes(), buffer, to)
|
||||
}
|
||||
|
||||
pub(crate) fn convert_endian_bytes(self, cls: EndianBytes, buffer: &mut [u8], to: ByteOrder) {
|
||||
if self == to {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: at MSRV 1.89 or higher use `slice::as_chunks_mut`.
|
||||
match cls {
|
||||
EndianBytes::One => {
|
||||
// No change needed
|
||||
}
|
||||
EndianBytes::Two => {
|
||||
for chunk in buffer.chunks_exact_mut(2) {
|
||||
let chunk: &mut [u8; 2] = chunk.try_into().unwrap();
|
||||
*chunk = u16::from_be_bytes(*chunk).to_le_bytes();
|
||||
}
|
||||
}
|
||||
EndianBytes::Four => {
|
||||
for chunk in buffer.chunks_exact_mut(4) {
|
||||
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
|
||||
*chunk = u32::from_be_bytes(*chunk).to_le_bytes();
|
||||
}
|
||||
}
|
||||
EndianBytes::Eight => {
|
||||
for chunk in buffer.chunks_exact_mut(8) {
|
||||
let chunk: &mut [u8; 8] = chunk.try_into().unwrap();
|
||||
*chunk = u64::from_be_bytes(*chunk).to_le_bytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of individual byte-order corrected elements.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum EndianBytes {
|
||||
One,
|
||||
Two,
|
||||
Four,
|
||||
Eight,
|
||||
}
|
||||
+208
-10
@@ -4,6 +4,7 @@ use tiff::decoder::{ifd, Decoder, DecodingResult};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||
@@ -45,12 +46,12 @@ fn test_image_color_type_unsupported(file: &str, expected_type: ColorType) {
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
assert!(match decoder.read_image() {
|
||||
assert!(matches!(
|
||||
decoder.read_image(),
|
||||
Err(tiff::TiffError::UnsupportedError(
|
||||
tiff::TiffUnsupportedError::UnsupportedColorType(_),
|
||||
)) => true,
|
||||
_ => false,
|
||||
});
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,7 +173,7 @@ fn test_string_tags() {
|
||||
&s,
|
||||
"GraphicsMagick 1.2 unreleased Q16 http://www.GraphicsMagick.org/"
|
||||
),
|
||||
_ => assert!(false),
|
||||
other => unreachable!("Expected Ascii tag from file, got {other:?}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -231,6 +232,54 @@ fn test_tiled_rect_rgb_u8() {
|
||||
test_image_sum_u8("tiled-rect-rgb-u8.tif", ColorType::RGB(8), 62081032);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_access() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("tiled-rect-rgb-u8.tif");
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(8));
|
||||
|
||||
let c = decoder
|
||||
.get_tag(tiff::tags::Tag::Compression)
|
||||
.unwrap()
|
||||
.into_u16()
|
||||
.unwrap();
|
||||
assert_eq!(c, tiff::tags::CompressionMethod::None.to_u16());
|
||||
|
||||
// Because the image is uncompressed, reading the first tile directly with the inner reader
|
||||
// should yield the same result as reading it with the decoder's read_chunk method.
|
||||
let first_offset = decoder
|
||||
.get_tag_u32_vec(tiff::tags::Tag::TileOffsets)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let first_byte_count = decoder
|
||||
.get_tag_u32_vec(tiff::tags::Tag::TileByteCounts)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
decoder
|
||||
.inner()
|
||||
.seek(std::io::SeekFrom::Start(first_offset as u64))
|
||||
.expect("Cannot seek to tile offset");
|
||||
let mut buf = vec![0; first_byte_count as usize];
|
||||
decoder
|
||||
.inner()
|
||||
.read_exact(&mut buf)
|
||||
.expect("Cannot read tile data");
|
||||
let raw_sum: u64 = buf.into_iter().map(<u64>::from).sum();
|
||||
|
||||
match decoder.read_chunk(0).unwrap() {
|
||||
DecodingResult::U8(chunk) => {
|
||||
let sum: u64 = chunk.into_iter().map(<u64>::from).sum();
|
||||
assert_eq!(sum, raw_sum);
|
||||
}
|
||||
_ => panic!("Wrong bit depth"),
|
||||
}
|
||||
}
|
||||
|
||||
/* #[test]
|
||||
fn test_tiled_jpeg_rgb_u8() {
|
||||
test_image_sum_u8("tiled-jpeg-rgb-u8.tif", ColorType::RGB(8), 93031606);
|
||||
@@ -292,6 +341,11 @@ fn test_tiled_incremental() {
|
||||
|
||||
#[test]
|
||||
fn test_planar_rgb_u8() {
|
||||
test_image_sum_u8("planar-rgb-u8.tif", ColorType::RGB(8), 15417630);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_planar_bands() {
|
||||
// gdal_translate tiled-rgb-u8.tif planar-rgb-u8.tif -co INTERLEAVE=BAND -co COMPRESS=LZW -co PROFILE=BASELINE
|
||||
let file = "planar-rgb-u8.tif";
|
||||
let expected_type = ColorType::RGB(8);
|
||||
@@ -328,8 +382,6 @@ fn test_planar_rgb_u8() {
|
||||
}
|
||||
_ => panic!("Wrong bit depth"),
|
||||
}
|
||||
|
||||
test_image_sum_u8(file, ColorType::RGB(8), 15417630);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -348,7 +400,7 @@ fn test_div_zero() {
|
||||
|
||||
match err {
|
||||
TiffError::FormatError(TiffFormatError::StripTileTagConflict) => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +418,7 @@ fn test_too_many_value_bytes() {
|
||||
|
||||
match error {
|
||||
tiff::TiffError::LimitsExceeded => {}
|
||||
unexpected => panic!("Unexpected error {}", unexpected),
|
||||
unexpected => panic!("Unexpected error {unexpected}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +549,7 @@ fn timeout() {
|
||||
|
||||
match error {
|
||||
TiffError::FormatError(TiffFormatError::CycleInOffsets) => {}
|
||||
e => panic!("Unexpected error {:?}", e),
|
||||
e => panic!("Unexpected error {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,3 +574,149 @@ fn test_zstd_compression() {
|
||||
// gdal_translate -co COMPRESS=ZSTD -co ZSTD_LEVEL=20 int16.tif int16_zstd.tif
|
||||
test_image_sum_i16("int16_zstd.tif", ColorType::Gray(16), 354396);
|
||||
}
|
||||
|
||||
fn test_image_bytes(
|
||||
file: &str,
|
||||
expected: &[(ColorType, u32)],
|
||||
// Represent all samples as little endian, ensuring check sums on all platforms.
|
||||
normalize_byte_order: fn(&mut [u8]),
|
||||
) {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
|
||||
let img_file = File::open(path).expect("Cannot find test image!");
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
|
||||
let mut buffer = vec![0u8; 0];
|
||||
for (idx, &(expected_type, expected_crc)) in expected.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
decoder
|
||||
.next_image()
|
||||
.expect("mismatched number of directories");
|
||||
}
|
||||
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
let layout = decoder
|
||||
.image_buffer_layout()
|
||||
.expect("Cannot describe buffer layout");
|
||||
|
||||
buffer.resize(layout.len, 0u8);
|
||||
decoder.read_image_bytes(&mut buffer).unwrap();
|
||||
normalize_byte_order(&mut buffer);
|
||||
|
||||
// Well almost a crc..
|
||||
assert_eq!(crc32fast::hash(&buffer), expected_crc);
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u16(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(2) {
|
||||
let n = u16::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u32(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(4) {
|
||||
let n = u32::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_order_u64(bytes: &mut [u8]) {
|
||||
for chunk in bytes.chunks_exact_mut(8) {
|
||||
let n = u64::from_ne_bytes(chunk.try_into().unwrap());
|
||||
chunk.copy_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_cmyk_3c_8b() {
|
||||
test_image_bytes(
|
||||
"cmyk-3c-8b.tiff",
|
||||
&[(ColorType::CMYK(8), 2935032230)],
|
||||
|_| (),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_16b() {
|
||||
test_image_bytes(
|
||||
"minisblack-1c-16b.tiff",
|
||||
&[(ColorType::Gray(16), 3524597615)],
|
||||
byte_order_u16,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_32b() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-32b.tiff",
|
||||
&[(ColorType::Gray(32), 2257036530)],
|
||||
byte_order_u32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_u64() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-64b.tiff",
|
||||
&[(ColorType::Gray(64), 2156547113)],
|
||||
byte_order_u64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_gray_f32() {
|
||||
test_image_bytes(
|
||||
"gradient-1c-32b-float.tiff",
|
||||
&[(ColorType::Gray(32), 3232356730)],
|
||||
byte_order_u32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fax")]
|
||||
fn test_fax4() {
|
||||
test_image_sum_u8("fax4.tiff", ColorType::Gray(1), 7802706);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fax")]
|
||||
fn test_fax4_white_is_min() {
|
||||
test_image_sum_u8("imagemagick_group4.tiff", ColorType::Gray(1), 3742820);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_extra_bits_image() {
|
||||
use tiff::{encoder, tags::ExtraSamples};
|
||||
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("extra_bits_rgb_8b.tiff");
|
||||
let img_file = File::create(path).expect("Cannot find test image!");
|
||||
|
||||
let mut encoder = encoder::TiffEncoder::new(img_file).expect("Cannot create encoder");
|
||||
let mut img = encoder.new_image::<encoder::colortype::RGB8>(8, 8).unwrap();
|
||||
|
||||
img.extra_samples(&[ExtraSamples::Unspecified]).unwrap();
|
||||
|
||||
// all pixels are supposed to be 0, the extra is 1
|
||||
let data: [u8; 256] = core::array::from_fn(|i| (i as u8 % 4).saturating_sub(2));
|
||||
|
||||
img.write_data(&data)
|
||||
.expect("correct amount of data to write");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_bits_gray() {
|
||||
test_image_sum_u8(
|
||||
"extra_bits_gray_8b.tiff",
|
||||
ColorType::Multiband {
|
||||
bit_depth: 8,
|
||||
num_samples: 2,
|
||||
},
|
||||
64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_bits_rgb() {
|
||||
test_image_sum_u8("extra_bits_rgb_8b.tiff", ColorType::RGB(8), 0);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use tiff::decoder::{Decoder, TiffCodingUnit};
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||
|
||||
#[test]
|
||||
fn read_all_planes() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
|
||||
let file = File::open(path).unwrap();
|
||||
let io = std::io::BufReader::new(file);
|
||||
|
||||
let mut tif = Decoder::new(io).unwrap();
|
||||
let layout = tif.image_buffer_layout().unwrap();
|
||||
assert_eq!(layout.planes, 3);
|
||||
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
|
||||
|
||||
eprintln!("{:?}", layout);
|
||||
let mut buffer = vec![0u8; layout.complete_len];
|
||||
tif.read_image_bytes(&mut buffer).unwrap();
|
||||
|
||||
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
|
||||
// depend on the file itself that is being used.
|
||||
const PLANE_EXCPECTED_SUMS: [u32; 3] = [15417630, 13007788, 11103530];
|
||||
|
||||
for (plane, expected) in (0..layout.planes).zip(PLANE_EXCPECTED_SUMS) {
|
||||
let plane_data = &buffer[plane_stride * plane..][..layout.len];
|
||||
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
|
||||
assert_eq!(sum, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_first_coding_unit() {
|
||||
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
|
||||
let file = File::open(path).unwrap();
|
||||
let io = std::io::BufReader::new(file);
|
||||
|
||||
let mut tif = Decoder::new(io).unwrap();
|
||||
let layout = tif.image_coding_unit_layout(TiffCodingUnit(0)).unwrap();
|
||||
|
||||
assert_eq!(layout.planes, 3);
|
||||
let plane_stride = layout.plane_stride.map_or(0, |x| x.get());
|
||||
eprintln!("{:?}", layout);
|
||||
let mut buffer = vec![0u8; layout.complete_len];
|
||||
|
||||
struct Expected {
|
||||
plane_sums: [u32; 3],
|
||||
unit: TiffCodingUnit,
|
||||
}
|
||||
|
||||
// Mainly you can see: these are different and non-zero. Otherwise these magic constants just
|
||||
// depend on the file itself that is being used. Also this varies by what unit we are reading.
|
||||
const PLANE_EXCPECTED_SUMS: [Expected; 2] = [
|
||||
Expected {
|
||||
plane_sums: [856853, 767686, 688532],
|
||||
unit: TiffCodingUnit(0),
|
||||
},
|
||||
Expected {
|
||||
plane_sums: [883679, 797329, 711086],
|
||||
unit: TiffCodingUnit(1),
|
||||
},
|
||||
];
|
||||
|
||||
for expected in PLANE_EXCPECTED_SUMS {
|
||||
tif.read_coding_unit_bytes(expected.unit, &mut buffer)
|
||||
.unwrap();
|
||||
|
||||
for (plane, expected) in (0..layout.planes).zip(expected.plane_sums) {
|
||||
let plane_data = &buffer[plane_stride * plane..][..layout.len];
|
||||
let sum: u32 = plane_data.iter().copied().map(u32::from).sum();
|
||||
assert_eq!(sum, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-9
@@ -57,7 +57,7 @@ fn encode_decode() {
|
||||
),
|
||||
(Tag::Compression, ifd::Value::Short(1)),
|
||||
(Tag::PhotometricInterpretation, ifd::Value::Short(2)),
|
||||
(Tag::StripOffsets, ifd::Value::Unsigned(47)),
|
||||
(Tag::StripOffsets, ifd::Value::Unsigned(35)),
|
||||
(Tag::SamplesPerPixel, ifd::Value::Short(3)),
|
||||
(Tag::RowsPerStrip, ifd::Value::Unsigned(3334)),
|
||||
(Tag::StripByteCounts, ifd::Value::Unsigned(30000)),
|
||||
@@ -81,7 +81,10 @@ fn encode_decode() {
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let tiff::decoder::DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -161,7 +164,9 @@ fn encode_decode_big() {
|
||||
decoder.get_tag(Tag::Artist).unwrap(),
|
||||
ifd::Value::Ascii("Image-tiff".into())
|
||||
);
|
||||
if let DecodingResult::U8(img_res) = decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let _layout = decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::U8(img_res) = buffer {
|
||||
assert_eq!(image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
@@ -235,6 +240,7 @@ const TEST_IMAGE_DIR: &str = "./tests/images/";
|
||||
|
||||
macro_rules! test_roundtrip {
|
||||
($name:ident, $buffer:ident, $buffer_ty:ty) => {
|
||||
#[track_caller]
|
||||
fn $name<C: colortype::ColorType<Inner = $buffer_ty>>(
|
||||
file: &str,
|
||||
expected_type: ColorType,
|
||||
@@ -244,7 +250,10 @@ macro_rules! test_roundtrip {
|
||||
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
|
||||
assert_eq!(decoder.colortype().unwrap(), expected_type);
|
||||
|
||||
let image_data = match decoder.read_image().unwrap() {
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
|
||||
let image_data = match buffer {
|
||||
DecodingResult::$buffer(res) => res,
|
||||
_ => panic!("Wrong data type"),
|
||||
};
|
||||
@@ -258,9 +267,11 @@ macro_rules! test_roundtrip {
|
||||
}
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
{
|
||||
let mut buffer = tiff::decoder::DecodingResult::U8(vec![]);
|
||||
let mut decoder = Decoder::new(&mut file).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = decoder.read_image().unwrap() {
|
||||
assert_eq!(image_data, img_res);
|
||||
decoder.read_image_to_buffer(&mut buffer).unwrap();
|
||||
if let DecodingResult::$buffer(img_res) = buffer {
|
||||
assert_eq!(*image_data, img_res);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
@@ -621,7 +632,7 @@ fn test_rows_per_strip() {
|
||||
let img2 = [i; 2 * 100];
|
||||
match decoder.read_chunk(i as u32).unwrap() {
|
||||
DecodingResult::U8(data) => assert_eq!(&img2[..], &data[..]),
|
||||
other => panic!("Incorrect strip type {:?}", other),
|
||||
other => panic!("Incorrect strip type {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,8 +663,6 @@ fn test_auxiliary_directory() {
|
||||
encoder.write_tag(Tag::ExifDirectory, exif.offset).unwrap();
|
||||
}
|
||||
|
||||
drop(tiff);
|
||||
|
||||
data.set_position(0);
|
||||
let mut decoder = Decoder::new(&mut data).unwrap();
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
assert_eq!(
|
||||
match decoder.read_image() {
|
||||
Ok(DecodingResult::U16(image_data)) => image_data,
|
||||
unexpected => panic!("Descoding RGB failed: {:?}", unexpected),
|
||||
unexpected => panic!("Descoding RGB failed: {unexpected:?}"),
|
||||
},
|
||||
image_rgb.reference_data()
|
||||
);
|
||||
@@ -126,7 +126,7 @@ fn encode_decode_with_compression(compression: Compression) {
|
||||
assert_eq!(
|
||||
match decoder.read_image() {
|
||||
Ok(DecodingResult::U8(image_data)) => image_data,
|
||||
unexpected => panic!("Decoding grayscale failed: {:?}", unexpected),
|
||||
unexpected => panic!("Decoding grayscale failed: {unexpected:?}"),
|
||||
},
|
||||
image_grayscale.reference_data()
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1,7 +1,7 @@
|
||||
extern crate tiff;
|
||||
|
||||
use tiff::decoder::{Decoder, DecodingResult};
|
||||
use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder};
|
||||
use tiff::encoder::{colortype, Predictor, TiffEncoder};
|
||||
use tiff::ColorType;
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
Reference in new issue
Block a user