Compare commits

...
Author SHA1 Message Date
A. Molzer eb6275547d Build Directory FromIterator
This enables a test for the encoder's DirectoryOffset but it seems to
fit nicely with the current design, too.
2025-11-12 01:21:53 +01:00
A. Molzer 73f066c09d Expose DirectoryOffset::new 2025-11-12 01:21:52 +01:00
Aurelia Molzer 24887896ff Merge pull request #309 from image-rs/short-writes-uncompressed
Short writes uncompressed
2025-11-08 15:52:36 +00:00
A. Molzer 02118620e2 Verify handling of a blocked writer 2025-11-08 11:52:58 +01:00
A. Molzer 0b5dc72454 Fix: uncompressed encoder doing short writes 2025-11-08 11:01:52 +01:00
Aurelia Molzer 34e35fb27e Merge pull request #307 from image-rs/public-byte-order
Provide a `tags::ByteOrder` public API
2025-11-05 03:40:13 +00:00
Aurelia Molzer bec6558aa8 Merge pull request #306 from 1c3t3a/fix-panic-on-invalid-lzw
Don't panic if LZW doesn't find an LZW end code
2025-11-04 22:22:12 +00:00
A. Molzer 6cdf3d8d22 Naming pass for byte order conversion 2025-11-04 23:17:05 +01:00
A. Molzer 0a2b3f91ab Remove unused method in EndianReader 2025-11-04 12:55:29 +01:00
A. Molzer 99b3c03ce3 Use bulk byte order conversion for tag reads 2025-11-04 12:54:53 +01:00
Aurelia Molzer bec8e495a5 Merge pull request #308 from image-rs/vectorized-predict
const-specialize predict by sample counts
2025-11-04 10:37:21 +00:00
A. Molzer de7dbad18b const-specialize predict by sample counts
The idea is to allow codegen to exploit specific known constants. It
seems that LLVM by itself will not generate split loop blocks by an
input argument that is a loop constant. Whereas we know, from PNG, that
some constants yield much better code. In this case

    coef[i] = coef[i] + coef[i - n]

This has a destructive dependency chain for `n = 1`, should never get an
argument of 0 and is almost embarrassingly parallel in SIMD if n >= 8
where we can increase the amount of data loaded at once for each
independent loop iteration.

By splitting the loop we make the compiler apply independent
optimization passes to each of case, then have a fallback for things we
did not cover with vectorized possibilities.
2025-11-04 10:23:51 +01:00
A. Molzer 19ac30ac3f Share ByteOrder in tags, publicly 2025-11-03 20:29:48 +01:00
A. Molzer 2058b7f3ca Stand-alone size computation of Type 2025-11-03 20:29:48 +01:00
Bastian Kersting edd260fe7e Don't panic if LZW doesn't find an LZW end code
Currently, when getting the LZWStatus::NoProgress from the underlying
weezl library, tiff checks that no input bytes were read and no
output bytes were written. Although this is according to what the
weezl library mentions in their doc-comment, it's not what the real
result always looks like.

Our fuzzer found input that triggered exactly this case, where the LZW
reader is able to write some output byte before hitting the NoProgress
status.

This change removes the assert, as the resulting action is anyways to return
an error that can be handled by the caller.
2025-10-31 06:17:35 +00:00
Aurelia Molzer 2ccbe132e0 Merge pull request #305 from Mrmaxmeier/fix-malformed-sample-format
Fix decoder panic with malformed (empty) sample format tag
2025-10-27 15:01:59 +00:00
Your Name 714e470595 Fix decoder panic with malformed (empty) sample format tag
I've included a minimal reproducer in
tests/fuzz_images/oor_panic/sample-format-empty-vec.bin
2025-10-27 15:56:37 +01:00
Jacob Adam W. 4adba46b9f Update zune-jpeg to 0.5 (#304) 2025-10-25 13:53:04 -07:00
Aurelia Molzer 695208cffe Merge pull request #298 from image-rs/release-0.10.3
Add metadata and changelog for 0.10.3
2025-09-01 00:33:39 +00:00
Aurelia Molzer 4acdfea783 Add metadata and changelog for 0.10.3 2025-09-01 02:30:40 +02:00
Aurelia Molzer 64286c2110 Merge pull request #294 from image-rs/fax-default-feature
Make fax a default feature
2025-09-01 00:15:14 +00:00
Aurelia Molzer 12c98535f6 Merge pull request #297 from image-rs/fax4-photometric-interpretation
Verify WhiteIsZero invert, add Gray(1) color type
2025-09-01 00:14:14 +00:00
Aurelia Molzer 1556713d6e Simplify match in invert 2025-09-01 02:12:10 +02:00
Aurelia Molzer 682ef0c375 Comment on fax3/4 RLE treatment in tiff 2025-09-01 01:59:42 +02:00
Aurelia Molzer bc71086a96 Verify WhiteIsZero invert, add Gray(1) color type
This was found through the fax4 decoding that is always bilevel.

No longer silently ignore an unknown color type when a photometric
interpretation requires an inversion. DICOM requires that WhiteIsZero
and BlackIsZero are only used for samples-per-pixel=1 but we do not
strictly enforce that early. Previously for the result Multiband color
we just ignored the tag which is also not right.
2025-09-01 00:25:48 +02:00
Sergey "Shnatsel" Davidoff e67bac763f Merge pull request #295 from image-rs/fax-supported-readme
Document Fax compression as supported
2025-08-31 20:38:38 +01:00
Sergey "Shnatsel" Davidoff 839655b975 Clarify it's Fax4 only, fax3 is not supported 2025-08-31 20:36:17 +01:00
Sergey "Shnatsel" Davidoff 85635c3cd2 Document Fax compression as supported 2025-08-31 19:18:43 +01:00
Sergey "Shnatsel" Davidoff 481a2ab54a Make fax a default feature
Fax was commonly requested, so let's make it Just Work when tiff crate is used through `image` where it's difficult to fiddle with tiff crate features.
2025-08-31 15:28:09 +01:00
Aurelia Molzer 7c6c327817 Merge pull request #229 from stephenjudkins/sdj/fax4-support
CCITT group 4 (Fax4) decoding support
2025-08-31 11:58:05 +00:00
Aurelia Molzer 9f1de91880 Make an explicit 'fax' feature 2025-08-31 01:32:32 +02:00
Aurelia Molzer 4a68feab5a Apply clippy lints 2025-08-31 01:26:05 +02:00
Aurelia Molzer b5afb8cca4 Drop bitvec dependency 2025-08-31 01:25:58 +02:00
Stephen Judkins c8d43262e9 Simplify decoder
Reuse the allocation of bits for every row though
2025-08-31 01:25:51 +02:00
Stephen Judkins 80ce77190e Refactor Group4 reader into stream module 2025-08-31 01:25:43 +02:00
Stephen Judkins c75aa98ce2 bit-addressable output (doesn't work yet) 2025-08-31 01:25:42 +02:00
Stephen Judkins c1087f6f08 Reader instance for group 4-decoded images, that buffer one line at a time 2025-08-31 01:24:21 +02:00
Aurelia Molzer 8cc6bc657d Merge pull request #293 from 1c3t3a/fix-byte-metadata-array
Fix type mismatch for reading Lists of Bytes
2025-08-30 21:33:48 +00:00
Stephen Judkins 9fcc2a3b65 review changes: pass in width/height, unwrap reader bytes 2025-08-30 23:24:30 +02:00
Stephen Judkins 666f871d30 CCITT group 4 (Fax4) decoding support 2025-08-30 23:20:41 +02:00
Bastian Kersting a919b8febf Fix type mismatch for reading Lists of Bytes
At the moment when writing Byte strings into tiff metadata sections (like
for XMP metadata) and try to read it with this crate, you will find your
characters as a Value::UnsignedBig, holding the char value with padding.

This seems wrong and we should instead return actual Value::Bytes.
2025-08-29 12:06:03 +00:00
Aurelia Molzer 85b5df7a62 Merge pull request #292 from image-rs/changelog-0.10.1
Changelog 0.10.2
2025-08-26 16:03:34 +00:00
Aurelia Molzer 756ea3e813 Add metadata for 0.10.2 2025-08-26 11:24:04 +02:00
Aurelia Molzer 0eead083f6 Fix missing method on Decoder 2025-08-26 11:19:35 +02:00
Aurelia Molzer 34232d8141 Merge pull request #291 from image-rs/changelog-0.10.1
Populate changelog for 0.10.1
2025-08-25 21:42:34 +00:00
Aurelia Molzer cbcf6b97d4 Bump version number 0.10.1 2025-08-25 23:21:34 +02:00
Sergey "Shnatsel" Davidoff 19c811e86d populate changelog for 0.10.1 2025-08-25 15:20:04 +01:00
Aurelia Molzer 50be15e356 Merge pull request #290 from jakubadamw/nightly-compilation-warnings
Address compilation warnings in the Nightly channel of Rust
2025-08-14 16:50:22 +00:00
Jacob Adam 718f2cb455 Address compilation warnings in the Nightly channel of Rust. 2025-08-14 17:45:21 +01:00
19 changed files with 878 additions and 234 deletions

No files matched your search

+41
View File
@@ -1,3 +1,44 @@
# Version 0.10.4 (unreleased)
- `Directory` now implements `FromIterator<(Tag, Value)>`.
# 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:
+7 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "tiff"
version = "0.10.0"
version = "0.10.3"
edition = "2021"
resolver = "2"
@@ -20,21 +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"
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"]
+1
View File
@@ -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 |
+226 -120
View File
@@ -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
@@ -447,11 +440,11 @@ impl Entry {
Type::BYTE => Byte(self.offset[0]),
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(())
}
}
+26 -9
View File
@@ -9,6 +9,7 @@ use crate::tags::{
use crate::{
ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
};
use std::io::{self, Cursor, Read, Seek};
use std::num::NonZeroUsize;
use std::sync::Arc;
@@ -144,12 +145,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,
};
@@ -343,6 +347,9 @@ 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 => {
match self.samples {
1 => Ok(ColorType::Gray(self.bits_per_sample)),
@@ -384,7 +391,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),
@@ -436,13 +445,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);
}
@@ -452,6 +462,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),
@@ -658,6 +674,7 @@ impl Image {
compression_method,
*compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims,
)?;
if output_row_stride == chunk_row_bytes {
@@ -674,7 +691,7 @@ impl Image {
);
}
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
@@ -691,7 +708,7 @@ 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 {
@@ -713,7 +730,7 @@ impl Image {
predictor,
);
if photometric_interpretation == PhotometricInterpretation::WhiteIsZero {
super::invert_colors(row, color_type, self.sample_format);
super::invert_colors(row, color_type, self.sample_format)?;
}
}
}
+105 -36
View File
@@ -138,7 +138,7 @@ impl DecodingResult {
}
}
pub fn as_buffer(&mut self, start: usize) -> DecodingBuffer {
pub fn as_buffer(&mut self, start: usize) -> DecodingBuffer<'_> {
match *self {
DecodingResult::U8(ref mut buf) => DecodingBuffer::U8(&mut buf[start..]),
DecodingResult::U16(ref mut buf) => DecodingBuffer::U16(&mut buf[start..]),
@@ -368,27 +368,82 @@ pub struct IfdDecoder<'lt> {
}
fn rev_hpredict_nsamp(buf: &mut [u8], bit_depth: u8, samples: usize) {
match bit_depth {
0..=8 => {
fn one_byte_predict<const N: usize>(buf: &mut [u8]) {
for i in N..buf.len() {
buf[i] = buf[i].wrapping_add(buf[i - N]);
}
}
fn two_bytes_predict<const N: usize>(buf: &mut [u8]) {
for i in (2 * N..buf.len()).step_by(2) {
let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap());
let p = u16::from_ne_bytes(buf[i - 2 * N..][..2].try_into().unwrap());
buf[i..][..2].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes());
}
}
fn four_bytes_predict<const N: usize>(buf: &mut [u8]) {
for i in (N * 4..buf.len()).step_by(4) {
let v = u32::from_ne_bytes(buf[i..][..4].try_into().unwrap());
let p = u32::from_ne_bytes(buf[i - 4 * N..][..4].try_into().unwrap());
buf[i..][..4].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes());
}
}
match (bit_depth, samples) {
// Note we can't use `windows` or so due to the overlap between each iteration. We split
// the cases by the samples / lookback constant so that each is optimized individually.
// This is more code generated but each loop can then have a different vectorization
// strategy.
(0..=8, 1) => one_byte_predict::<1>(buf),
(0..=8, 2) => one_byte_predict::<2>(buf),
(0..=8, 3) => one_byte_predict::<3>(buf),
(0..=8, 4) => one_byte_predict::<4>(buf),
// The generic, sub-optimal case for the above.
(0..=8, _) => {
for i in samples..buf.len() {
buf[i] = buf[i].wrapping_add(buf[i - samples]);
}
}
9..=16 => {
(9..=16, 1) => {
two_bytes_predict::<1>(buf);
}
(9..=16, 2) => {
two_bytes_predict::<2>(buf);
}
(9..=16, 3) => {
two_bytes_predict::<3>(buf);
}
(9..=16, 4) => {
two_bytes_predict::<4>(buf);
}
(9..=16, _) => {
for i in (samples * 2..buf.len()).step_by(2) {
let v = u16::from_ne_bytes(buf[i..][..2].try_into().unwrap());
let p = u16::from_ne_bytes(buf[i - 2 * samples..][..2].try_into().unwrap());
buf[i..][..2].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes());
}
}
17..=32 => {
(17..=32, 1) => {
four_bytes_predict::<1>(buf);
}
(17..=32, 2) => {
four_bytes_predict::<2>(buf);
}
(17..=32, 3) => {
four_bytes_predict::<3>(buf);
}
(17..=32, 4) => {
four_bytes_predict::<4>(buf);
}
(17..=32, _) => {
for i in (samples * 4..buf.len()).step_by(4) {
let v = u32::from_ne_bytes(buf[i..][..4].try_into().unwrap());
let p = u32::from_ne_bytes(buf[i - 4 * samples..][..4].try_into().unwrap());
buf[i..][..4].copy_from_slice(&(v.wrapping_add(p)).to_ne_bytes());
}
}
33..=64 => {
(33..=64, _) => {
for i in (samples * 8..buf.len()).step_by(8) {
let v = u64::from_ne_bytes(buf[i..][..8].try_into().unwrap());
let p = u64::from_ne_bytes(buf[i - 8 * samples..][..8].try_into().unwrap());
@@ -475,11 +530,22 @@ fn fix_endianness_and_predict(
}
}
fn invert_colors(buf: &mut [u8], color_type: ColorType, sample_format: SampleFormat) {
fn invert_colors(
buf: &mut [u8],
color_type: ColorType,
sample_format: SampleFormat,
) -> TiffResult<()> {
match (color_type, sample_format) {
(ColorType::Gray(8), SampleFormat::Uint) => {
// Where pixels do not cross a byte boundary
(ColorType::Gray(1 | 2 | 4 | 8), SampleFormat::Uint) => {
for x in buf {
*x = 0xff - *x;
// Equivalent to both of the following:
//
// *x = 0xff - *x
// *x = !*x
//
// since -x = !x+1
*x = !*x;
}
}
(ColorType::Gray(16), SampleFormat::Uint) => {
@@ -512,42 +578,31 @@ fn invert_colors(buf: &mut [u8], color_type: ColorType, sample_format: SampleFor
x.copy_from_slice(&(1.0 - v).to_ne_bytes());
}
}
_ => {}
_ => {
return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnknownInterpretation,
))
}
}
Ok(())
}
/// Fix endianness. If `byte_order` matches the host, then conversion is a no-op.
fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u8) {
match byte_order {
ByteOrder::LittleEndian => match bit_depth {
0..=8 => {}
9..=16 => buf.chunks_exact_mut(2).for_each(|v| {
v.copy_from_slice(&u16::from_le_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
17..=32 => buf.chunks_exact_mut(4).for_each(|v| {
v.copy_from_slice(&u32::from_le_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
_ => buf.chunks_exact_mut(8).for_each(|v| {
v.copy_from_slice(&u64::from_le_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
},
ByteOrder::BigEndian => match bit_depth {
0..=8 => {}
9..=16 => buf.chunks_exact_mut(2).for_each(|v| {
v.copy_from_slice(&u16::from_be_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
17..=32 => buf.chunks_exact_mut(4).for_each(|v| {
v.copy_from_slice(&u32::from_be_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
_ => buf.chunks_exact_mut(8).for_each(|v| {
v.copy_from_slice(&u64::from_be_bytes((*v).try_into().unwrap()).to_ne_bytes())
}),
},
let host = ByteOrder::native();
let class = match bit_depth {
0..=8 => crate::tags::EndianBytes::One,
9..=16 => crate::tags::EndianBytes::Two,
17..=32 => crate::tags::EndianBytes::Four,
_ => crate::tags::EndianBytes::Eight,
};
host.convert_endian_bytes(class, buf, byte_order);
}
impl<R: Read + Seek> Decoder<R> {
/// Create a new decoder that decodes from the stream ```r```
pub fn new(mut r: R) -> TiffResult<Decoder<R>> {
let mut endianess = Vec::with_capacity(2);
(&mut r).take(2).read_to_end(&mut endianess)?;
@@ -731,6 +786,11 @@ impl<R: Read + Seek> Decoder<R> {
}
/// Returns the byte_order of the file.
///
/// # Usage
///
/// This is only relevant to interpreting raw bytes read from tags. The image decoding methods
/// will correct to the host byte order automatically.
pub fn byte_order(&self) -> ByteOrder {
self.value_reader.reader.byte_order
}
@@ -1281,6 +1341,15 @@ impl<R: Read + Seek> Decoder<R> {
self.image_ifd().find_tag_unsigned(tag)
}
/// Tries to retrieve a vector of all a tag's values and convert them to the desired unsigned
/// type.
pub fn find_tag_unsigned_vec<T: TryFrom<u64>>(
&mut self,
tag: Tag,
) -> TiffResult<Option<Vec<T>>> {
self.image_ifd().find_tag_unsigned_vec(tag)
}
/// Tries to retrieve a tag from the current image directory and convert it to the desired
/// unsigned type. Returns an error if the tag is not present.
pub fn get_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<T> {
+101 -23
View File
@@ -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> {
@@ -187,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",
@@ -276,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::*;
+30
View File
@@ -104,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 {
@@ -118,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;
+1 -1
View File
@@ -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)
}
}
+127 -13
View File
@@ -37,8 +37,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 +48,6 @@ pub enum Compression {
Packbits,
}
impl Default for Compression {
fn default() -> Self {
Self::Uncompressed
}
}
impl Compression {
fn tag(&self) -> CompressionMethod {
match self {
@@ -79,10 +74,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 +171,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 +180,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 +193,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 +244,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 +261,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>,
@@ -318,6 +321,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 +348,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()
}
@@ -791,6 +810,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 +943,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);
}
+28 -28
View File
@@ -15,7 +15,7 @@ pub trait TiffValue {
/// Access this value as an contiguous sequence of bytes.
/// If their is no trivial representation, allocate it on the heap.
fn data(&self) -> Cow<[u8]>;
fn data(&self) -> Cow<'_, [u8]>;
/// Write this value to a TiffWriter.
/// While the default implementation will work in all cases, it may require unnecessary allocations.
@@ -34,7 +34,7 @@ impl TiffValue for [u8] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(self)
}
}
@@ -47,7 +47,7 @@ impl TiffValue for [i8] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(self))
}
}
@@ -60,7 +60,7 @@ impl TiffValue for [u16] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(self))
}
}
@@ -73,7 +73,7 @@ impl TiffValue for [i16] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(self))
}
}
@@ -86,7 +86,7 @@ impl TiffValue for [u32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(self))
}
}
@@ -99,7 +99,7 @@ impl TiffValue for [i32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(self))
}
}
@@ -112,7 +112,7 @@ impl TiffValue for [u64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(self))
}
}
@@ -125,7 +125,7 @@ impl TiffValue for [i64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(self))
}
}
@@ -138,7 +138,7 @@ impl TiffValue for [f32] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe
Cow::Borrowed(bytecast::f32_as_ne_bytes(self))
}
@@ -152,7 +152,7 @@ impl TiffValue for [f64] {
self.len()
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
// We write using native endian so this should be safe
Cow::Borrowed(bytecast::f64_as_ne_bytes(self))
}
@@ -171,7 +171,7 @@ impl TiffValue for u8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(from_ref(self))
}
}
@@ -189,7 +189,7 @@ impl TiffValue for i8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i8_as_ne_bytes(from_ref(self)))
}
}
@@ -207,7 +207,7 @@ impl TiffValue for u16 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u16_as_ne_bytes(from_ref(self)))
}
}
@@ -225,7 +225,7 @@ impl TiffValue for i16 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i16_as_ne_bytes(from_ref(self)))
}
}
@@ -243,7 +243,7 @@ impl TiffValue for u32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(self)))
}
}
@@ -261,7 +261,7 @@ impl TiffValue for i32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i32_as_ne_bytes(from_ref(self)))
}
}
@@ -279,7 +279,7 @@ impl TiffValue for u64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(self)))
}
}
@@ -297,7 +297,7 @@ impl TiffValue for i64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::i64_as_ne_bytes(from_ref(self)))
}
}
@@ -315,7 +315,7 @@ impl TiffValue for f32 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f32_as_ne_bytes(from_ref(self)))
}
}
@@ -333,7 +333,7 @@ impl TiffValue for f64 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::f64_as_ne_bytes(from_ref(self)))
}
}
@@ -351,7 +351,7 @@ impl TiffValue for Ifd {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u32_as_ne_bytes(from_ref(&self.0)))
}
}
@@ -369,7 +369,7 @@ impl TiffValue for Ifd8 {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Borrowed(bytecast::u64_as_ne_bytes(from_ref(&self.0)))
}
}
@@ -388,7 +388,7 @@ impl TiffValue for Rational {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::u32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::u32_as_ne_bytes(from_ref(&self.d));
@@ -411,7 +411,7 @@ impl TiffValue for SRational {
Ok(())
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
let first_dword = bytecast::i32_as_ne_bytes(from_ref(&self.n));
let second_dword = bytecast::i32_as_ne_bytes(from_ref(&self.d));
@@ -438,7 +438,7 @@ impl TiffValue for str {
}
}
fn data(&self) -> Cow<[u8]> {
fn data(&self) -> Cow<'_, [u8]> {
Cow::Owned({
if self.is_ascii() && !self.bytes().any(|b| b == 0) {
let bytes: &[u8] = self.as_bytes();
@@ -462,7 +462,7 @@ impl<T: TiffValue + ?Sized> TiffValue for &'_ 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());
+55
View File
@@ -190,3 +190,58 @@ 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];
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 data = tiff_reader.read_image().unwrap();
assert!(matches!(data, crate::decoder::DecodingResult::U8(v) if v == imgdata));
}
}
+3
View File
@@ -187,6 +187,9 @@ quick_error! {
InsufficientOutputBufferSize { needed: usize, provided: usize } {
display("the borrowed output buffer is not large enough for the decoded data, needed {needed} but have {provided}")
}
ZeroIfdPointer {
display("the offset 0 can not point to a valid IFD")
}
}
}
+115
View File
@@ -193,6 +193,45 @@ 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.
@@ -271,3 +310,79 @@ pub enum SampleFormat(u16) unknown(
Void = 4,
}
}
/// 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,
}
+12
View File
@@ -672,3 +672,15 @@ fn bytes_gray_f32() {
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);
}
Binary file not shown.
Binary file not shown.