Compare commits

..
Author SHA1 Message Date
A. Molzer 32dda6e11f Add integration test for exif copy 2026-01-15 20:21:54 +01:00
A. Molzer 74191f92bb Change EXIF Applicability's None to passthrough
This allows it to address the case of copying all EXIF data into a
separate file. And as soon as our decoder can handle files without any
image data (missing any stripoffsets, etc.) in ways that do not return
errors we can hopefully do the opposite and merge the data again.
2026-01-15 20:21:54 +01:00
A. Molzer f057b5f47d Reduce tag_filter to a minimum, implement copy 2026-01-15 20:21:52 +01:00
A. Molzer b3ced063ef Write tag filter based on tag kind/'Level' 2026-01-14 17:40:40 +01:00
A. Molzer 76721c6893 Add conversion of ValueBuffer to IfdPointer
This mirrors what the other value type provides, for now private and
only for IfdPointer which we need for iterating an existing directory.
2026-01-14 17:40:40 +01:00
A. Molzer 348bbda906 Sketch a TagFilter option for copying metadata 2026-01-13 23:46:39 +01:00
Aurelia Molzer 1bd6293857 Merge pull request #329 from image-rs/fixup-value-buffer-debug-condition
Fix debug condition of ValueBuffer
2026-01-13 14:53:12 +00:00
A. Molzer 87341f9075 Fix debug condition of ValueBuffer
Off-by-one, this was meant to check a minimum length that includes
equality but instead tested a valid index.
2026-01-13 15:37:22 +01:00
Aurelia Molzer 43343f64cd Merge pull request #328 from image-rs/reduce-package-size
Remove benchmark images from distribution
2026-01-13 09:12:59 +00:00
Aurelia Molzer be04326264 Remove benchmark images from distribution 2026-01-13 10:06:59 +01:00
Aurelia Molzer 93a68f52f5 Merge pull request #327 from image-rs/release-0.11.1
Metadata for release 0.11.1
2026-01-13 09:04:30 +00:00
A. Molzer 5f329840c8 Metadata for release 0.11.1 2026-01-12 00:43:29 +01:00
Aurelia Molzer 1d138a5568 Merge pull request #326 from image-rs/encode-values-from-buffer
Encode values from buffer
2026-01-11 22:09:29 +00:00
A. Molzer 9a23090b6e Provide write_entry_bytes
This complements the entry API of the encoder with the byte slice based
method. Note we don't provide it for writing raw data, here a
`std::io::Write` would be more appropriate and we should not attempt to
make it `write_all`, nor compress. You'd get all the benefits of the
other API through `[u8]: TiffValue` since that interface is otherwise
type agnostic and `u8` specifically is endian agnostic.
2026-01-11 22:59:34 +01:00
A. Molzer 9d409433e8 Rename write_tag_value to write_tag_buf
We have not released this yet, let's align these names before it is too
late. The decoder adopted the `_buf` suffix and I find it more
convincing than the more generic `_value` (that also confusingly shares
it with `TiffValue`).
2026-01-11 22:29:08 +01:00
Aurelia Molzer cf680424d9 Merge pull request #325 from image-rs/decode-values-into-buffer
Decode tag values into buffer
2026-01-11 19:19:41 +00:00
A. Molzer d3b777c961 Testing access through alternate tag reads 2026-01-11 17:55:15 +01:00
A. Molzer 24b008a7c3 Read tag values into byte slice 2026-01-11 17:20:04 +01:00
A. Molzer 34bd1da99e Read raw values into ValueBuffer 2026-01-11 17:20:04 +01:00
Aurelia Molzer cfff04c97d Merge pull request #324 from image-rs/encoder-tiff-value
A byte-valued endian-formatted tiff value buffer
2026-01-10 02:47:17 +00:00
A. Molzer 915cc20945 Revert "Construct tags::ValueBuffer from ifd::Value"
This reverts commit 45f617b9f5
2026-01-10 01:27:43 +01:00
A. Molzer 74b2857364 Construct ValueBuffer from TiffValue 2026-01-10 01:27:43 +01:00
A. Molzer 1dab89f10e Generalize write for pre-encoded tags 2026-01-10 01:27:25 +01:00
A. Molzer 45f617b9f5 Construct tags::ValueBuffer from ifd::Value
Provides interaction between decoder and encoder values. Not sure about
this commit yet, the implementation is a bit odd.
2026-01-09 14:49:02 +01:00
A. Molzer 170bd3697c Provide tags in their byte encoding 2026-01-09 14:42:22 +01:00
Aurelia Molzer 855eb4ef23 Merge pull request #323 from image-rs/tiff-value-for-tags
Utils for working with tags
2026-01-09 08:11:24 +00:00
A. Molzer fee650d7f6 Ensure repr and values of tags! variants 2026-01-08 19:53:47 +01:00
A. Molzer ee8d13249e Utils for working with tags
Implement the encoder's `TiffValue` trait for the tags commonly defined
and represented as separate types in our own code. We don't tie the
tag for an entry of these to the value type (yet). This lets us keep the
trait exactly as-is and we avoid unnecessary churn. We can always add a
separate trait on top.
2026-01-08 19:53:40 +01:00
Aurelia Molzer 169a896851 Merge pull request #322 from image-rs/deprecate-big-rational-types
Deprecate unofficial value types
2026-01-08 17:16:20 +00:00
A. Molzer 082a35adb9 Deprecate unofficial value types
Seems they were introduced with BigTiff but since the enumeration of the
decoder had no obligation of introducing discriminant values the list
was not cross-referenced to libtiff. Hence, some rational types exist
with no basis in decoding.
2026-01-08 14:25:09 +01:00
Aurelia Molzer 923df5f0bd Merge pull request #321 from image-rs/forward-tiff-value-for-vec-and-array
Add diagnostic helps for TiffValue proxy implementations
2026-01-06 20:05:30 +00:00
A. Molzer c4328644b8 Fixup clippy from MSRV 1.85 bump 2026-01-06 17:30:51 +01:00
A. Molzer 27bfcf5562 Bump MSRV to 1.85
This gives us the diagnostic items. As a side effect we will have
`expect` for lints from 1.81 to apply in later PRs. It's time I think.
2026-01-06 17:27:51 +01:00
A. Molzer 7d498a0284 Add TiffValue proxy implementations
We have a fundamental choice here: We could add more proxy
implementations for `Vec` and `Box` to follow `&'_ T`, for some types
that indirections is clear. Vec and arrays are also both very natural
ways of expressing a repeated value. However, passing an allocated value
to the interface forces us to drop it inside. We might be able to
utilize the allocation for an in-place byteswap in a future increment
but not with the current design.

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

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

Now they see:

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

Previously the compiler would both strip the `&` in its first message
(due to our forwarding impl) and not list any way of getting a
reference-to-slice.
2026-01-06 17:26:18 +01:00
Aurelia Molzer 5b21bcc75c Merge pull request #320 from image-rs/release-0.11
Release notes for 0.11
2026-01-05 22:53:18 +00:00
12 changed files with 1341 additions and 47 deletions

No files matched your search

+4 -4
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
rust: ["1.74.0", stable, beta, nightly] rust: ["1.85.0", stable, beta, nightly]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly - uses: dtolnay/rust-toolchain@nightly
if: ${{ matrix.rust == '1.74.0' }} if: ${{ matrix.rust == '1.85.0' }}
- name: Generate Cargo.lock with minimal-version dependencies - name: Generate Cargo.lock with minimal-version dependencies
if: ${{ matrix.rust == '1.74.0' }} if: ${{ matrix.rust == '1.85.0' }}
run: cargo -Zminimal-versions generate-lockfile run: cargo -Zminimal-versions generate-lockfile
- uses: dtolnay/rust-toolchain@v1 - uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
- name: build - name: build
run: cargo build -v run: cargo build -v
- name: test - name: test
if: ${{ matrix.rust != '1.74.0' }} if: ${{ matrix.rust != '1.85.0' }}
run: cargo test -v && cargo doc -v run: cargo test -v && cargo doc -v
features: features:
+31
View File
@@ -1,3 +1,34 @@
# Version 0.11.1
Fixes:
- The enumeration types in `tags` are now all marked with a representation of
their underlying TIFF type (e.g. `repr(u16)`) and variants are explicitly
assigned their corresponding values. That is you may _read_ the raw
discriminant and interpret that as the value—except for `Unknown` variants.
- The variants `RationalBig` and `SRationalBig` of `decoder::ifd::Value` are
deprecated as they have no corresponding TIFF type and were not constructed
during decoding.
Additions:
- Types in `tags` now generally implement `TiffValue` and can be handed to the
`DirectoryEncoder::write_tag` method. Unlike primitive types they do _not_
always implement the trait for slices of themselves.
- Added `tags::ValueBuffer`, a byte-based buffer for tags with a runtime value
type and count.
- Added `IfdDecoder::find_tag_{buf,bytes}` to read tag values into a
`ValueBuffer` or byte slice respectively without interpreting them
immediately within the Rust type system.
- Added `DirectoryEncoder::write_{data,tag}_buf` that takes a `ValueBuffer`
instead of a statically typed argument.
- Added `DirectoryEncoder::write_entry{,_buf,_bytes}` that encode their
argument into the file and return an `Entry` for later use, without
immediately adding a tag-entry pair to the directory.
- The `TiffValue` trait is now implemented for several builtin `tags::*` types
or slices of them as appropriate for their use as values of their intended
TIFF tag.
- The `TiffValue` trait is now implemented for `[T; N]` for all types that
implement it for slices.
# Version 0.11.0 # Version 0.11.0
- `Directory` now implements `FromIterator<(Tag, Value)>`. - `Directory` now implements `FromIterator<(Tag, Value)>`.
+3 -3
View File
@@ -1,11 +1,11 @@
[package] [package]
name = "tiff" name = "tiff"
version = "0.11.0" version = "0.11.1"
edition = "2021" edition = "2021"
resolver = "2" resolver = "2"
# note: when changed, also update test runner in `.github/workflows/rust.yml` # note: when changed, also update test runner in `.github/workflows/rust.yml`
rust-version = "1.74.0" rust-version = "1.85.0"
license = "MIT" license = "MIT"
description = "TIFF decoding and encoding library in pure Rust" description = "TIFF decoding and encoding library in pure Rust"
@@ -14,7 +14,7 @@ authors = ["The image-rs Developers"]
repository = "https://github.com/image-rs/image-tiff" repository = "https://github.com/image-rs/image-tiff"
categories = ["multimedia::images", "multimedia::encoding"] categories = ["multimedia::images", "multimedia::encoding"]
exclude = ["tests/images/*", "tests/fuzz_images/*"] exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
[dependencies] [dependencies]
half = { version = "2.4.1" } half = { version = "2.4.1" }
+144 -12
View File
@@ -6,12 +6,12 @@ use std::mem;
use std::str; use std::str;
use super::stream::{ByteOrder, EndianReader}; use super::stream::{ByteOrder, EndianReader};
use crate::tags::{IfdPointer, Tag, Type}; use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
use crate::{TiffError, TiffFormatError, TiffResult}; use crate::{TiffError, TiffFormatError, TiffResult};
use self::Value::{ use self::Value::{
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig, Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
}; };
#[allow(unused_qualifications)] #[allow(unused_qualifications)]
@@ -30,8 +30,16 @@ pub enum Value {
Double(f64), Double(f64),
List(Vec<Value>), List(Vec<Value>),
Rational(u32, u32), Rational(u32, u32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
RationalBig(u64, u64), RationalBig(u64, u64),
SRational(i32, i32), SRational(i32, i32),
#[deprecated(
note = "Not implemented in BigTIFF with a standard tag value",
since = "0.11.1"
)]
SRationalBig(i64, i64), SRationalBig(i64, i64),
Ascii(String), Ascii(String),
Ifd(u32), Ifd(u32),
@@ -170,7 +178,8 @@ impl Value {
Unsigned(val) => Ok(vec![val]), Unsigned(val) => Ok(vec![val]),
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]), UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
Rational(numerator, denominator) => Ok(vec![numerator, denominator]), Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
RationalBig(numerator, denominator) => { #[expect(deprecated)]
Value::RationalBig(numerator, denominator) => {
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?]) Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
} }
Ifd(val) => Ok(vec![val]), Ifd(val) => Ok(vec![val]),
@@ -220,7 +229,8 @@ impl Value {
new_vec.push(numerator); new_vec.push(numerator);
new_vec.push(denominator); new_vec.push(denominator);
} }
SRationalBig(numerator, denominator) => { #[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(i32::try_from(numerator)?); new_vec.push(i32::try_from(numerator)?);
new_vec.push(i32::try_from(denominator)?); new_vec.push(i32::try_from(denominator)?);
} }
@@ -234,7 +244,8 @@ impl Value {
Signed(val) => Ok(vec![val]), Signed(val) => Ok(vec![val]),
SignedBig(val) => Ok(vec![i32::try_from(val)?]), SignedBig(val) => Ok(vec![i32::try_from(val)?]),
SRational(numerator, denominator) => Ok(vec![numerator, denominator]), SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
SRationalBig(numerator, denominator) => { #[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?]) Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
} }
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
@@ -283,7 +294,8 @@ impl Value {
Unsigned(val) => Ok(vec![val.into()]), Unsigned(val) => Ok(vec![val.into()]),
UnsignedBig(val) => Ok(vec![val]), UnsignedBig(val) => Ok(vec![val]),
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), #[expect(deprecated)]
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
Ifd(val) => Ok(vec![val.into()]), Ifd(val) => Ok(vec![val.into()]),
IfdBig(val) => Ok(vec![val]), IfdBig(val) => Ok(vec![val]),
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()), Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
@@ -301,7 +313,8 @@ impl Value {
new_vec.push(numerator.into()); new_vec.push(numerator.into());
new_vec.push(denominator.into()); new_vec.push(denominator.into());
} }
SRationalBig(numerator, denominator) => { #[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => {
new_vec.push(numerator); new_vec.push(numerator);
new_vec.push(denominator); new_vec.push(denominator);
} }
@@ -315,7 +328,8 @@ impl Value {
Signed(val) => Ok(vec![val.into()]), Signed(val) => Ok(vec![val.into()]),
SignedBig(val) => Ok(vec![val]), SignedBig(val) => Ok(vec![val]),
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), #[expect(deprecated)]
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)), _ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
} }
} }
@@ -565,7 +579,7 @@ impl Entry {
// Case 4: there is more than one value, and it doesn't fit in the offset field. // Case 4: there is more than one value, and it doesn't fit in the offset field.
let mut v; let mut v;
self.set_reader_offset(bo, bigtiff, reader)?; self.set_reader_offset_relative(bigtiff, reader, 0)?;
match self.type_ { match self.type_ {
Type::BYTE | Type::UNDEFINED => { Type::BYTE | Type::UNDEFINED => {
@@ -723,6 +737,113 @@ impl Entry {
Ok(List(v)) Ok(List(v))
} }
pub(crate) fn buffered_value<R: Read + Seek>(
&self,
buf: &mut ValueBuffer,
limits: &super::Limits,
bigtiff: bool,
reader: &mut EndianReader<R>,
) -> TiffResult<()> {
if self.count == 0 {
buf.assume_type(self.type_, 0, reader.byte_order);
return Ok(());
}
let value_bytes = self.buffer_with_capacity(buf, limits)?;
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
let src = &self.offset[..value_bytes];
buf.raw_bytes_mut()[..value_bytes].copy_from_slice(src);
buf.assume_type(self.type_, self.count, reader.byte_order);
return Ok(());
}
// Case 2: the value is stored in the reader at an offset.
self.set_reader_offset_relative(bigtiff, reader, 0)?;
// In case of an error we set the type and endianess.
buf.assume_type(self.type_, 0, reader.byte_order);
let target = &mut buf.raw_bytes_mut()[..value_bytes];
// FIXME: if the read fails we have already grown to full size, which is not great.
reader.inner().read_exact(target)?;
buf.assume_type(self.type_, self.count, reader.byte_order);
Ok(())
}
pub(crate) fn raw_value_at<R: Read + Seek>(
&self,
buf: &mut [u8],
bigtiff: bool,
reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<usize> {
if self.count == 0 {
return Ok(0);
}
// We have no limits to handle, we do not allocate.
let value_bytes = self.type_.value_bytes(self.count)?;
// No bytes to fill into the buffer.
if at >= value_bytes {
return Ok(0);
}
// Case 1: the value fits in the offset field.
if value_bytes <= 4 || bigtiff && value_bytes <= 8 {
// `at < value_bytes` and `value_bytes <= 8` so casting is mathematical
let src = &self.offset[..value_bytes as usize][at as usize..];
let len = src.len().min(buf.len());
buf[..len].copy_from_slice(&src[..len]);
return Ok(value_bytes as usize);
}
// Case 2: the value is stored in the reader at an offset. We will find the offset
// encoded in the entry, apply the relative start position and seek there.
self.set_reader_offset_relative(bigtiff, reader, at)?;
let remainder = value_bytes - at;
let len = usize::try_from(remainder)
.unwrap_or(usize::MAX)
.min(buf.len());
let target = &mut buf[..len];
reader.inner().read_exact(target)?;
// Design note: in a previous draft we would consume the rest of the bytes of this value
// here (into a stack buffer if need be) to verify the stream itself. But in the end we
// have `Seek` so we better verify this by seeking over the rest of the bytes, finding if
// the stream continues that far. Even that is maybe bad if we wanted to provide a
// async-adaptor that `WouldBlock` errors to fill back a read window then the seek is
// poison to that, too.
// So a really simple choice: The caller is responsible for handling the fact that this did
// not verify the whole value. Attempt a 1-byte read at the end of the value instead?
Ok(len)
}
// Returns `Ok(bytes)` if our value's bytes through type and count fit into `usize` and are
// within the limits. Extends the buffer to that many bytes.
fn buffer_with_capacity(
&self,
buf: &mut ValueBuffer,
limits: &super::Limits,
) -> TiffResult<usize> {
let bytes = self.type_.value_bytes(self.count())?;
let allowed_length = usize::try_from(bytes)
.ok()
.filter(|&n| n <= limits.decoding_buffer_size)
.ok_or(TiffError::LimitsExceeded)?;
buf.prepare_length(allowed_length);
Ok(allowed_length)
}
fn vec_with_capacity( fn vec_with_capacity(
value_count: u64, value_count: u64,
limits: &super::Limits, limits: &super::Limits,
@@ -736,21 +857,32 @@ impl Entry {
Ok(Vec::with_capacity(value_count)) Ok(Vec::with_capacity(value_count))
} }
fn set_reader_offset<R>( /// Seek to an offset within a value stored in the offset defined by this entry.
fn set_reader_offset_relative<R>(
&self, &self,
bo: ByteOrder,
bigtiff: bool, bigtiff: bool,
reader: &mut EndianReader<R>, reader: &mut EndianReader<R>,
at: u64,
) -> TiffResult<()> ) -> TiffResult<()>
where where
R: Read + Seek, R: Read + Seek,
{ {
let bo = reader.byte_order;
let offset = if bigtiff { let offset = if bigtiff {
self.offset_field_reader(bo).read_u64()? self.offset_field_reader(bo).read_u64()?
} else { } else {
self.offset_field_reader(bo).read_u32()?.into() self.offset_field_reader(bo).read_u32()?.into()
}; };
// FIXME: `at` should be within `self.type_.value_bytes(self.count)` and that itself should
// be within the bounds of the stream. But we do not check this eagerly so this below will
// fail sometimes differently for exotic streams, depending on the method by which we read
// (at once or through multiple raw into-byte-slice reads).
let offset = offset.checked_add(at).ok_or(TiffError::FormatError(
TiffFormatError::InconsistentSizesEncountered,
))?;
reader.goto_offset(offset)?; reader.goto_offset(offset)?;
Ok(()) Ok(())
+97 -1
View File
@@ -5,7 +5,7 @@ use std::num::NonZeroUsize;
use crate::tags::{ use crate::tags::{
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor, CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
SampleFormat, Tag, Type, SampleFormat, Tag, Type, ValueBuffer,
}; };
use crate::{ use crate::{
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
@@ -1717,12 +1717,41 @@ impl<R: Seek + Read> ValueReader<R> {
} }
impl IfdDecoder<'_> { impl IfdDecoder<'_> {
/// Retrieve the IFD entry for a given tag, if it exists.
///
/// The entry contains the metadata of the value, that is its type and count from which we can
/// calculate a total byte size.
pub fn find_entry(&self, tag: Tag) -> Option<ifd::Entry> {
self.inner.ifd.get(tag).cloned()
}
/// Tries to retrieve a tag. /// Tries to retrieve a tag.
/// Return `Ok(None)` if the tag is not present. /// Return `Ok(None)` if the tag is not present.
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> { pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
self.inner.find_tag(tag) self.inner.find_tag(tag)
} }
/// Retrieve a tag and reproduce its bytes into the provided buffer.
///
/// The buffer is unmodified if the tag is not present.
pub fn find_tag_buf(
&mut self,
tag: Tag,
buf: &mut ValueBuffer,
) -> TiffResult<Option<ifd::Entry>> {
self.inner.find_tag_buf(tag, buf)
}
/// Read bytes of a tag's value into a byte buffer.
pub fn find_tag_bytes(
&mut self,
tag: Tag,
buf: &mut [u8],
offset: u64,
) -> TiffResult<Option<usize>> {
self.inner.find_tag_raw(tag, buf, offset)
}
/// Tries to retrieve a tag and convert it to the desired unsigned type. /// Tries to retrieve a tag and convert it to the desired unsigned type.
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> { pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
self.find_tag(tag)? self.find_tag(tag)?
@@ -1836,3 +1865,70 @@ impl<'l> IfdDecoder<'l> {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::Decoder;
use crate::{
bytecast,
tags::{ByteOrder, Tag, ValueBuffer},
};
#[test]
fn equivalence_of_tag_readers() {
let file = std::fs::File::open(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/images/int8_rgb.tif"
))
.unwrap();
let mut decoder = Decoder::new(file).unwrap();
let file_bo = decoder.byte_order();
let mut ifd = decoder.image_ifd();
{
let value = ifd
.find_tag(Tag::BitsPerSample)
.unwrap()
.expect("must have BitsPerSample");
let samples = value.into_u16_vec().unwrap();
assert_eq!(samples.as_slice(), [8, 8, 8]);
}
{
let mut value = ValueBuffer::from_value(&[0u16; 4]);
let _entry = ifd
.find_tag_buf(Tag::BitsPerSample, &mut value)
.unwrap()
.expect("must have BitsPerSample");
value.set_byte_order(ByteOrder::native());
assert_eq!(value.as_bytes(), bytecast::u16_as_ne_bytes(&[8, 8, 8]));
}
{
let mut by_bytes = [0u16; 4];
let entry = ifd
.find_entry(Tag::BitsPerSample)
.expect("must have BitsPerSample");
let byte_len = ifd
.find_tag_bytes(
Tag::BitsPerSample,
bytecast::u16_as_ne_mut_bytes(&mut by_bytes),
0,
)
.unwrap()
.expect("must have BitsPerSample");
assert_eq!(byte_len, 3 * std::mem::size_of::<u16>());
file_bo.convert(
entry.field_type(),
bytecast::u16_as_ne_mut_bytes(&mut by_bytes[..3]),
ByteOrder::native(),
);
assert_eq!(&by_bytes[..3], &[8, 8, 8]);
}
}
}
+50 -2
View File
@@ -1,7 +1,9 @@
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use crate::{tags::Tag, Directory}; use crate::{
use crate::{TiffError, TiffFormatError, TiffResult}; tags::{Tag, ValueBuffer},
Directory, TiffError, TiffFormatError, TiffResult,
};
use super::ifd::{Entry, Value}; use super::ifd::{Entry, Value};
use super::ValueReader; use super::ValueReader;
@@ -14,12 +16,25 @@ pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
pub(crate) trait EntryDecoder { pub(crate) trait EntryDecoder {
/// Turn an entry into a value by fetching the required bytes in the stream. /// Turn an entry into a value by fetching the required bytes in the stream.
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>; fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value>;
/// Fill a `ValueBuffer` by fetching the required bytes.
fn entry_buf(&mut self, entry: &Entry, _: &mut ValueBuffer) -> TiffResult<()>;
/// Fill the prefix of the buffer and return the actual bytes. Start interpreting the inner
/// value at `offset`.
fn entry_raw(&mut self, entry: &Entry, _: &mut [u8], offset: u64) -> TiffResult<usize>;
} }
impl<R: Seek + Read> EntryDecoder for ValueReader<R> { impl<R: Seek + Read> EntryDecoder for ValueReader<R> {
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> { fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
entry.val(&self.limits, self.bigtiff, &mut self.reader) entry.val(&self.limits, self.bigtiff, &mut self.reader)
} }
fn entry_buf(&mut self, entry: &Entry, buf: &mut ValueBuffer) -> TiffResult<()> {
entry.buffered_value(buf, &self.limits, self.bigtiff, &mut self.reader)
}
fn entry_raw(&mut self, entry: &Entry, buf: &mut [u8], offset: u64) -> TiffResult<usize> {
entry.raw_value_at(buf, self.bigtiff, &mut self.reader, offset)
}
} }
impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> { impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
@@ -32,6 +47,39 @@ impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
self.decoder.entry_val(&entry).map(Some) self.decoder.entry_val(&entry).map(Some)
} }
/// Find a tag and fill the provided buffer with its raw data.
///
/// Returns the entry of the tag if found. This signals that the buffer was overwritten.
/// Otherwise the buffer is preserved as-is.
pub(crate) fn find_tag_buf(
&mut self,
tag: Tag,
buf: &mut ValueBuffer,
) -> TiffResult<Option<Entry>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
self.decoder.entry_buf(&entry, buf)?;
Ok(Some(entry))
}
pub(crate) fn find_tag_raw(
&mut self,
tag: Tag,
buf: &mut [u8],
at: u64,
) -> TiffResult<Option<usize>> {
let entry = match self.ifd.get(tag) {
None => return Ok(None),
Some(entry) => entry.clone(),
};
let len = self.decoder.entry_raw(&entry, buf, at)?;
Ok(Some(len))
}
pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> { pub(crate) fn require_tag(&mut self, tag: Tag) -> TiffResult<Value> {
match self.find_tag(tag)? { match self.find_tag(tag)? {
Some(val) => Ok(val), Some(val) => Ok(val),
+367 -23
View File
@@ -2,24 +2,25 @@ pub use tiff_value::*;
use std::{ use std::{
cmp, cmp,
io::{self, Seek, Write}, io::{self, Read, Seek, Write},
marker::PhantomData, marker::PhantomData,
mem, mem,
num::{NonZeroU64, TryFromIntError}, num::{NonZeroU64, TryFromIntError},
}; };
use crate::{ use crate::{
decoder::ifd::Entry, decoder::{ifd::Entry, Decoder},
error::{TiffResult, UsageError}, error::{TiffResult, UsageError},
tags::{ tags::{
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit, ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
SampleFormat, Tag, Type, PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
}, },
Directory, TiffError, TiffFormatError, Directory, TiffError, TiffFormatError,
}; };
pub mod colortype; pub mod colortype;
pub mod compression; pub mod compression;
mod tag_filter;
mod tiff_value; mod tiff_value;
mod writer; mod writer;
@@ -40,6 +41,8 @@ pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")] #[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel; pub type DeflateLevel = compression::DeflateLevel;
pub use tag_filter::TagFilter;
#[derive(Clone, Copy, PartialEq, Default)] #[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression { pub enum Compression {
#[default] #[default]
@@ -301,12 +304,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
value.write(&mut writer)?; value.write(&mut writer)?;
} }
let entry = Self::write_value( let entry = Self::write_entry_inner(
self.writer, self.writer,
&DirectoryEntry { DirectoryEntry {
data_type: <T>::FIELD_TYPE, data_type: <T>::FIELD_TYPE,
count: value.count().try_into()?, count: value.count().try_into()?,
data: bytes, data: bytes.into(),
}, },
)?; )?;
@@ -315,15 +318,114 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
Ok(()) Ok(())
} }
/// Write a tag-value pair with prepared byte data.
///
/// Note that the library will _not_ attempt to verify that the data type or the count of the
/// buffered value is permissible for the given tag. The data will be associated with the tag
/// exactly as-is.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
pub fn write_tag_buf(&mut self, tag: Tag, value: &ValueBuffer) -> TiffResult<()> {
let entry = self.write_entry_buf(value)?;
self.directory.extend([(tag, entry)]);
Ok(())
}
/// Write some data to the tiff file, the offset of the data is returned. /// Write some data to the tiff file, the offset of the data is returned.
/// ///
/// This could be used to write tiff strips. /// This could be used to write tiff strips. All of the data will be written into the file as a
/// slice of bytes. It can not be used as an entry in a directory directly, where very short
/// values are instead represented in the offset field directly. Use [`Self::write_entry`]
/// instead.
pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> { pub fn write_data<T: TiffValue>(&mut self, value: T) -> TiffResult<u64> {
let offset = self.writer.offset(); let offset = self.writer.offset();
value.write(self.writer)?; value.write(self.writer)?;
Ok(offset) Ok(offset)
} }
/// Write some data directly to the tiff file, the offset of the data is returned.
///
/// All of the data will be written into the file as a slice of bytes. It can not be used as an
/// entry in a directory directly, where very short values are instead represented in the
/// offset field directly. Use [`Self::write_entry_buf`] instead.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
pub fn write_data_buf(&mut self, value: &ValueBuffer) -> TiffResult<u64> {
self.check_value_byteorder(value)?;
let offset = self.writer.offset();
self.writer.write_bytes(value.as_bytes())?;
Ok(offset)
}
/// Write a directory value into the file.
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry<T: TiffValue>(&mut self, value: T) -> TiffResult<Entry> {
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: T::FIELD_TYPE,
count: K::convert_offset(value.count() as u64)?,
// FIXME: not optimal we always allocate here. But the only other method we have
// available is `T::write(&mut TiffWriter<W>)` and we must pass that into
// `write_entry_inner` but then have a combinatorial explosion of instantiations
// which is absurd.
data: value.data(),
},
)
}
/// Write a directory value into the file.
///
/// An error is returned if the byte order of the presented value does not match the byte order
/// of the underlying file (i.e. the [native byte order](`crate::tags::ByteOrder::native`)).
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry_buf(&mut self, value: &ValueBuffer) -> TiffResult<Entry> {
self.check_value_byteorder(value)?;
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: value.data_type(),
count: K::convert_offset(value.count())?,
data: value.as_bytes().into(),
},
)
}
/// Write a directory entry by its byte data.
///
/// The data must be pre-formatted to the byte order expected by the file.
///
/// Returns an [`Entry`]. If the value is short enough it will *not* be written in the file but
/// stored in the entry's offset field.
pub fn write_entry_bytes(&mut self, ty: Type, data: &[u8]) -> TiffResult<Entry> {
if data.len() % usize::from(ty.byte_len()) != 0 {
return Err(TiffError::UsageError(UsageError::MismatchedEntryLength {
ty,
found: data.len(),
}));
}
let count = data.len() / usize::from(ty.byte_len());
let count = u64::try_from(count)?;
Self::write_entry_inner(
self.writer,
DirectoryEntry {
data_type: ty,
count: K::convert_offset(count)?,
data: data.into(),
},
)
}
/// Insert previously written key-value entries. /// Insert previously written key-value entries.
/// ///
/// It is the caller's responsibility to ensure that the data referred to by the entries is /// It is the caller's responsibility to ensure that the data referred to by the entries is
@@ -335,6 +437,148 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
self.directory.extend(entries); self.directory.extend(entries);
} }
/// Copy metadata from the IFD that a decoder refers to.
///
/// You generally want to use [`ImageEncoder::write_metadata_copy_from`] which automatically
/// fills out the [`EncodeMetadataApplicability`] as applicable to the chosen color space and
/// other sample configuration.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
kind: EncodeMetadataApplicability,
) -> TiffResult<EncodeMetadataReport> {
let mut report = EncodeMetadataReport {
copied: 0,
filtered: 0,
};
enum Role {
Ifd,
ExifPrivate,
GpsPrivate,
}
let ptr = decoder.ifd_pointer().unwrap();
let mut stack = vec![(ptr, Role::Ifd, usize::MAX)];
let mut buf = ValueBuffer::empty(Type::UNDEFINED);
// Directories which we will later write in reverse-pre-order.
let mut pre_order_dirs: Vec<(Directory, usize, Tag)> = vec![];
while let Some((ifd, role, parent)) = stack.pop() {
let dir = decoder.read_directory(ifd)?;
let mut created_dir = Directory::empty();
let filter = match role {
Role::Ifd => TagFilter::filter_primary,
Role::ExifPrivate => TagFilter::filter_exif_private,
Role::GpsPrivate => TagFilter::filter_gps_private,
};
let mut ifd_reader = decoder.read_directory_tags(&dir);
for (tag, entry) in dir.iter() {
let level = filter(options.tag_filter, tag.to_u16());
match options
.tag_filter
.should_keep_for_image_writer(level, &kind)
{
tag_filter::Choice::Ok => {}
tag_filter::Choice::Descend(subifd_kind) => {
// Only single pointers can be directories to descend to, avoid an overly
// large read by pulling that value out of the file.
if entry.count() != 1 {
report.filtered = report.filtered.saturating_add(1);
continue;
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
// Should not happen (we're iterating the dict already), but be robust.
report.filtered = report.filtered.saturating_add(1);
continue;
};
let Some(ifd) = buf.as_ifd_pointer() else {
report.filtered = report.filtered.saturating_add(1);
continue;
};
let role = match subifd_kind {
tag_filter::SubifdKind::ExifPrivate => Role::ExifPrivate,
tag_filter::SubifdKind::Gps => Role::GpsPrivate,
tag_filter::SubifdKind::Interoperability => {
// Only applicable to compressed images.
report.filtered = report.filtered.saturating_add(1);
continue;
}
};
let this_as_parent = pre_order_dirs.len();
stack.push((ifd, role, this_as_parent));
}
tag_filter::Choice::Discard => continue,
tag_filter::Choice::Filtered
// Not supported yet.
| tag_filter::Choice::Special(tag_filter::Special::JpegThumbnail) => {
// Not to be copied.
report.filtered = report.filtered.saturating_add(1);
continue;
}
tag_filter::Choice::Special(tag_filter::Special::CompressedExclusive) => {
// We're not writing an App segment for a compressed file, are we?
continue;
}
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
report.filtered = report.filtered.saturating_add(1);
continue;
}
// Re-encode the value to the target file.
buf.set_byte_order(ByteOrder::native());
let entry = self.write_entry_buf(&buf)?;
created_dir.extend([(tag, entry)]);
report.copied = report.copied.saturating_add(1);
}
let tag_in_parent = match role {
// should be unreachable? For now. But thumbnails would use this so we best be a
// little cautious here. Also thumbnails are weird, they do have image data but we
// would not copy that data itself with these methods. Just eh.
//
// Anyways the tree root will use this tag as a dummy.
Role::Ifd => Tag::SubIfd,
Role::ExifPrivate => Tag::ExifDirectory,
Role::GpsPrivate => Tag::GpsDirectory,
};
pre_order_dirs.push((created_dir, parent, tag_in_parent));
}
// Now post-order traverse the directory tree.
while let Some((directory, parent, tag)) = pre_order_dirs.pop() {
// The root is the items for the IFD itself.
if pre_order_dirs.is_empty() {
// We do not enter ourselves into a parent directory.
let _ = (parent, tag);
self.extend_from(&directory);
break;
}
let offset = Self::write_directory_inner(self.writer, &directory)?;
let offset = K::convert_offset(offset)?;
let entry = self.write_entry(offset)?;
let (parent_dir, _, _) = &mut pre_order_dirs[parent];
parent_dir.extend([(tag, entry)]);
}
Ok(report)
}
/// Define the parent directory. /// Define the parent directory.
/// ///
/// Each directory has an offset based link to its successor, forming a linked list in the /// Each directory has an offset based link to its successor, forming a linked list in the
@@ -361,33 +605,41 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
} }
fn write_directory(&mut self) -> TiffResult<u64> { fn write_directory(&mut self) -> TiffResult<u64> {
Self::write_directory_inner(self.writer, &self.directory)
}
fn write_directory_inner(writer: &mut TiffWriter<W>, directory: &Directory) -> TiffResult<u64> {
// Start by turning all buffered unwritten values into entries. // Start by turning all buffered unwritten values into entries.
let offset = self.writer.offset(); let offset = writer.offset();
K::write_entry_count(self.writer, self.directory.len())?; K::write_entry_count(writer, directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>(); let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in self.directory.iter() { for (tag, entry) in directory.iter() {
self.writer.write_u16(tag.to_u16())?; writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?; writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?; let count = K::convert_offset(entry.count())?;
count.write(self.writer)?; count.write(writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?; writer.write_bytes(&entry.offset()[..offset_bytes])?;
} }
Ok(offset) Ok(offset)
} }
fn write_value( // Does it really make sense that this would be a (potentially) compressible write? For actual
// directory values their size must match the data written so there's going to be a silent
// problem if that actually occurs.
fn write_entry_inner(
writer: &mut TiffWriter<W>, writer: &mut TiffWriter<W>,
value: &DirectoryEntry<K::OffsetType>, value: DirectoryEntry<K::OffsetType>,
) -> TiffResult<Entry> { ) -> TiffResult<Entry> {
let &DirectoryEntry { let DirectoryEntry {
data: ref bytes, data: ref bytes,
ref count, ref count,
data_type, data_type,
} = value; } = value;
let in_entry_bytes = mem::size_of::<K::OffsetType>(); let in_entry_bytes = mem::size_of::<K::OffsetType>();
let mut offset_bytes = [0; 8]; let mut offset_bytes = [0; 8];
if bytes.len() > in_entry_bytes { if bytes.len() > in_entry_bytes {
@@ -406,6 +658,7 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
// and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it. // and some oversight, we can not clone the `count: K::OffsetType` and thus not convert it.
// Instead, write it to a buffer... // Instead, write it to a buffer...
let mut count_bytes = [0; 8]; let mut count_bytes = [0; 8];
debug_assert!(in_entry_bytes == 4 || in_entry_bytes == 8);
// Nominally Cow but we only expect `Cow::Borrowed`. // Nominally Cow but we only expect `Cow::Borrowed`.
count_bytes[..count.bytes()].copy_from_slice(&count.data()); count_bytes[..count.bytes()].copy_from_slice(&count.data());
@@ -419,6 +672,15 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
}) })
} }
fn check_value_byteorder(&self, value: &ValueBuffer) -> TiffResult<()> {
// FIXME: we should enable writing files with any chosen byte order.
if value.byte_order() != ByteOrder::native() {
Err(TiffError::UsageError(UsageError::ByteOrderMismatch))
} else {
Ok(())
}
}
/// Provides the number of bytes written by the underlying TiffWriter during the last call. /// Provides the number of bytes written by the underlying TiffWriter during the last call.
fn last_written(&self) -> u64 { fn last_written(&self) -> u64 {
self.writer.last_written() self.writer.last_written()
@@ -469,6 +731,72 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
} }
} }
#[non_exhaustive]
#[derive(Clone, Copy)]
pub struct EncodeMetadataOptions<'lt> {
/// Several metadata kinds contain additional tags in further, linked directories that are only
/// referenced with an `Ifd`/`Ifd8` tag. These directories may have their own semantics, e.g. a
/// different set of tags or even new value types.
///
/// Since we know that the whole directory contains only metadata it would be possible to copy
/// all tags contained in it, however tags that can not be semantically validated might contain
/// data that is only valid within the original image or must be transformed during the copy.
/// For instance, JPEG thumbnails are embedded with separate offset and length fields (`0x0201`
/// and `0x202`). (This has caused the author mental harm, please do not consult me about it).
///
/// Setting this flag will skip such unknown tags instead of attempting a potentially incorrect
/// copy. However the set of copied metadata is then dependent on the `tiff` library.
pub tag_filter: &'lt TagFilter,
// TODO:
// Some tags are not allowed to be recorded or can be semantically validated. The library can
// skip these tags while copying metadata. For instance, `YCbCrCoefficients` _may_ be
// recorded for images in `YCbCr` photometric interpretation but _must not_ be recorded for
// images in any other interpretation (colorspace conversion should have been done during the
// transformation).
// pub validate_tags: enum ValidateTags {
// None,
// Skip,
// Error,
// Strict /* Error but also do not allow anything that can not be validated */
// },
}
impl Default for EncodeMetadataOptions<'_> {
fn default() -> Self {
EncodeMetadataOptions {
tag_filter: TagFilter::only_known_tags(),
}
}
}
/// Describe the kind of image for which metadata is recorded.
///
/// Metadata that is not applicable to the color space or the layout may be skipped. For instance,
/// subsampling basis position is only relevant on YCbCr / Chroma color photometric interpretation
/// where subsampling is defined.
#[non_exhaustive]
#[derive(Clone, Copy, Default)]
pub struct EncodeMetadataApplicability {
/// The photometric interpretation of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// photometric interpretation are recorded.
pub photometric: Option<PhotometricInterpretation>,
/// The planar configuration of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// configuration are recorded.
pub planar: Option<PlanarConfiguration>,
}
#[non_exhaustive]
pub struct EncodeMetadataReport {
/// Number of tags transferred (recursively).
pub copied: u64,
/// Number of tags encountered that were skipped due to the provided filter.
pub filtered: u64,
}
/// Type to encode images strip by strip. /// Type to encode images strip by strip.
/// ///
/// You should call `finish` on this when you are finished with it. /// You should call `finish` on this when you are finished with it.
@@ -582,7 +910,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
)?; )?;
encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?; encoder.write_tag(Tag::XResolution, Rational { n: 1, d: 1 })?;
encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?; encoder.write_tag(Tag::YResolution, Rational { n: 1, d: 1 })?;
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?; encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
Ok(ImageEncoder { Ok(ImageEncoder {
encoder, encoder,
@@ -709,6 +1037,22 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
Ok(()) Ok(())
} }
/// Copy metadata from the IFD that a decoder refers to.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
) -> TiffResult<EncodeMetadataReport> {
self.encoder.write_metadata_copy_from(
decoder,
options,
EncodeMetadataApplicability {
photometric: Some(<T>::TIFF_VALUE),
planar: Some(PlanarConfiguration::Chunky),
},
)
}
/// Set image resolution /// Set image resolution
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) { pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
self.encoder self.encoder
@@ -766,7 +1110,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
} else { } else {
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec(); let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
let replicated = let replicated =
core::iter::repeat(<T>::BITS_PER_SAMPLE[0]).take(self.extra_samples.len()); core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
sample_format.extend(replicated); sample_format.extend(replicated);
self.encoder self.encoder
@@ -788,7 +1132,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
// extra samples and not provided via a slice used for samples themselves. // extra samples and not provided via a slice used for samples themselves.
.unwrap_or(SampleFormat::Void.to_u16()); .unwrap_or(SampleFormat::Void.to_u16());
sample_format.extend(core::iter::repeat(extra_format).take(self.extra_samples.len())); sample_format.extend(core::iter::repeat_n(extra_format, self.extra_samples.len()));
self.encoder self.encoder
.write_tag(Tag::SampleFormat, &sample_format[..])?; .write_tag(Tag::SampleFormat, &sample_format[..])?;
@@ -824,10 +1168,10 @@ impl<'a, W: Write + Seek, C: ColorType, K: TiffKind> Drop for ImageEncoder<'a, W
} }
} }
struct DirectoryEntry<S> { struct DirectoryEntry<'data, S> {
data_type: Type, data_type: Type,
count: S, count: S,
data: Vec<u8>, data: std::borrow::Cow<'data, [u8]>,
} }
/// Trait to abstract over Tiff/BigTiff differences. /// Trait to abstract over Tiff/BigTiff differences.
+256
View File
@@ -0,0 +1,256 @@
use crate::tags::{PhotometricInterpretation, PlanarConfiguration};
/// A filter of tags copied in
/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`].
pub struct TagFilter {
filter_level: FilterLevel,
}
impl TagFilter {
/// Return a filter that allows all tags from metadata subdirectories.
pub fn allow_all_in_subdirectories() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::All,
};
&INSTANCE
}
/// Return a filter that has an explicit allow list of tags, for each of a known list of
/// metadata subdirectories.
pub fn only_known_tags() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::Known,
};
&INSTANCE
}
/// FIXME: take a configuration by which to filter out complete metadata kinds.
/// FIXME: when we iterate over a whole directory of tags, this iteration is more expensive
/// than necessary. Tags within a directory should be ordered. We can thus perform a binary set
/// join which is more efficient than checking the individual filter on each. Does not matter
/// much in most cases though.
pub(crate) fn filter_primary(&self, tag: u16) -> Level {
let as_exif = self.filter_primary_exif(tag);
if !matches!(as_exif, Level::Unknown) {
return as_exif;
}
let as_gps = self.filter_primary_gps(tag);
if !matches!(as_gps, Level::Unknown) {
return as_gps;
}
let as_icc = self.filter_icc(tag);
if !matches!(as_icc, Level::Unknown) {
return as_icc;
}
Level::Unknown
}
/// As per EXIF V3.0 tags, Primary Image (0th IFD).
pub(crate) fn filter_primary_exif(&self, tag: u16) -> Level {
match tag {
// width, height, etc
0x0100..=0x0103 => Level::SampleLayout,
// photometric interpretation
0x0106 => Level::SampleLayout,
// data tied to the camera and customized description, potentially PI.
0x010f..=0x0110 => Level::Value,
// strip offsets
0x0111 => Level::SampleLayout,
// orientation
0x0112 => Level::Value,
// strip byte counts, samples
0x0115..=0x0117 => Level::SampleLayout,
// pixel dimensions (ratio). Should be generic enough.
0x011a..=0x011b => Level::Value,
// planar configuration
0x011c => Level::Photometric(Photometric::Planar),
// units for 0x011a,0x011b
0x0128 => Level::Value,
// transfer function
0x012d => Level::Photometric(Photometric::All),
// file data time, software pipeline. I'm wary of privacy implications.
0x0131..=0x0132 => Level::Value,
// artist, clearly PI unless you explicitly wish it to be attributed.
0x013b => Level::Value,
// Whitepoint, Chromaticities
0x013e..=0x013f => Level::Photometric(Photometric::All),
// The table lists these as Not allowed for anything but we may have a choice here for
// older models of images.
0x0201..=0x0202 => Level::Special(Special::JpegThumbnail),
0x0211..=0x0213 => Level::Photometric(Photometric::YCbCr),
0x0214 => Level::Photometric(Photometric::All),
// Copyright holder (not same as artist).
0x8298 => Level::Value,
0x8769 => Level::Ifd(SubifdKind::ExifPrivate),
_ => Level::Unknown,
}
}
pub(crate) fn filter_primary_gps(&self, tag: u16) -> Level {
match tag {
0x8825 => Level::Ifd(SubifdKind::Gps),
_ => Level::Unknown,
}
}
pub(crate) fn filter_icc(&self, tag: u16) -> Level {
match tag {
0x8ff3 => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn filter_exif_private(&self, tag: u16) -> Level {
match tag {
// Maybe this is not personal identifying but some equipment may be very specifically
// known through these. And it's not critical to displaying it correctly.
0x829a => Level::Value,
0x829d => Level::Value,
0x8822 => Level::Value,
0x8824 => Level::Value,
0x8827 => Level::Value,
// Opto-electric coefficients
0x8828 => Level::Value,
0x8830..=0x8835 => Level::Value,
0x9000 => Level::Value,
0x9003..=0x9004 => Level::Value,
0x9010..=0x9012 => Level::Value,
// Not allowed for uncompressed data!
0x9101..=0x9102 => Level::Special(Special::CompressedExclusive),
0x9201..=0x9209 => Level::Value,
0x920a => Level::Value,
0x9214 => Level::Value,
0x927c => Level::Value,
0x9286 => Level::Value,
0x9290..=0x9292 => Level::Value,
0x9400..=0x9405 => Level::Value,
0xa000 => Level::Value,
0xa001 => Level::Photometric(Photometric::All),
0xa002..=0xa003 => Level::Special(Special::CompressedExclusive),
0xa004 => Level::Value,
0xa005 => Level::Ifd(SubifdKind::Interoperability),
0xa20b..=0xa20c => Level::Value,
0xa20e..=0xa210 => Level::Value,
0xa214..=0xa215 => Level::Value,
0xa217 => Level::Value,
0xa300..=0xa302 => Level::Value,
0xa401..=0xa40c => Level::Value,
0xa420 => Level::Value,
0xa430..=0xa43c => Level::Value,
0xa460..=0xa462 => Level::Value,
0xa500 => Level::Photometric(Photometric::All),
_ => Level::Unknown,
}
}
pub(crate) fn filter_gps_private(&self, tag: u16) -> Level {
match tag {
0x0000..=0x001f => Level::Value,
_ => Level::Unknown,
}
}
pub(crate) fn should_keep_for_image_writer(
&self,
level: Level,
target: &super::EncodeMetadataApplicability,
) -> Choice {
match level {
Level::Unknown if matches!(self.filter_level, FilterLevel::All) => Choice::Filtered,
// Sample Layout is controlled by the writer (dimensions, color type, …). Do not copy!
Level::SampleLayout => Choice::Discard,
Level::Ifd(subifd_kind) => Choice::Descend(subifd_kind),
Level::Photometric(photometric) => {
match (photometric, target.planar, target.photometric) {
(Photometric::All, _, _) => Choice::Ok,
(Photometric::Planar, Some(PlanarConfiguration::Planar) | None, _) => {
Choice::Ok
}
(Photometric::YCbCr, _, Some(PhotometricInterpretation::YCbCr) | None) => {
Choice::Ok
}
_ => Choice::Discard,
}
}
Level::Value => Choice::Ok,
Level::Special(special) => Choice::Special(special),
_ => Choice::Discard,
}
}
}
pub(crate) enum Choice {
Ok,
Descend(SubifdKind),
/// Tag is inapplicable to be copied.
Discard,
/// Tag was chosen not to be copied.
Filtered,
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FilterLevel {
All,
Known,
}
/// The level of support for a value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Level {
Unknown,
/// Value describes the image data layout, which we will not want to write into the encoder's
/// directory where the image data is written later.
///
/// These are treated as privacy preserving.
SampleLayout,
/// Value is an IFD and must be copied by making a subdirectory.
///
/// These are treated as privacy preserving, independent of the tags within the subdirectory
/// which may not be. (For instance the presence of an empty directory is fine but we will omit
/// a GPS anyways).
Ifd(SubifdKind),
/// Value is related to photometric interpretation and should not be copied when the target
/// does not match the expected interpretation.
///
/// These are *generally* treated as privacy preserving.
Photometric(Photometric),
/// This is a value we know of.
/// FIXME: we do not differentiate by personal identifiable information and otherwise which is
/// quite important for some variations of copying metadata.
Value,
/// We recognize the tag but it can not be copied by itself through normal means.
///
/// Privacy treatment depends on the tag.
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Photometric {
All,
/// Do not copy for non-planar data.
Planar,
/// Do not copy for non-YCbCr data.
YCbCr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SubifdKind {
ExifPrivate,
Gps,
Interoperability,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Special {
/// Thumbnail which we regard as PI since we do not deep-clean the JPEG structure.
JpegThumbnail,
/// May only occur in compressed (JPEG APP segments) data.
CompressedExclusive,
}
+165 -1
View File
@@ -1,14 +1,27 @@
use std::{borrow::Cow, io::Write, slice::from_ref}; use std::{borrow::Cow, io::Write, slice::from_ref};
use crate::{bytecast, tags::Type, TiffError, TiffFormatError, TiffResult}; use crate::{bytecast, tags, tags::Type, TiffError, TiffFormatError, TiffResult};
use super::writer::TiffWriter; use super::writer::TiffWriter;
/// Trait for types that can be encoded in a tiff file /// Trait for types that can be encoded in a tiff file
#[diagnostic::on_unimplemented(
message = "the type `{Self}` does not implement `TiffValue`",
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
note = "the trait is implemented for shared references to values",
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
note = "the trait is implemented for arrays if it is implemented for a slice",
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
)]
pub trait TiffValue { pub trait TiffValue {
const BYTE_LEN: u8; const BYTE_LEN: u8;
const FIELD_TYPE: Type; const FIELD_TYPE: Type;
// FIXME: If we want this to mean 'count' in the sense of `Entry` it should return `u32` or
// `u64`, probably the latter with `convert_offset` taking care of the check against `u32::MAX`
// for classic tiff.
fn count(&self) -> usize; fn count(&self) -> usize;
fn bytes(&self) -> usize { fn bytes(&self) -> usize {
self.count() * usize::from(Self::BYTE_LEN) self.count() * usize::from(Self::BYTE_LEN)
} }
@@ -450,6 +463,136 @@ impl TiffValue for str {
} }
} }
// FIXME: held up on reading the discriminant for all but the unknown variant.
//
// It needs to have `repr(u16)` to have a defined layout and then we may read the tag from its
// value representation by casting a u16 pointer to a pointer to the enum type.
// unsafe { *<*const _>::from(self).cast::<u16>() }
// But that is quite unsafe and needs careful review to maintain the safety requirements. Maybe we
// could get `bytemuck` to add a trait for this (it has `TransparentWrapper`) with a derive for
// enum types that have an explicit repr.
//
// This would allow returning borrowed data in more cases. For all types without an unknown, in all
// cases including as slices, and for all types with and unknown variant when we have a single
// value (either returning a borrowed discriminant or a borrowed value within `Unknown`).
impl TiffValue for tags::CompressionMethod {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
let bytes = self.to_u16().to_ne_bytes();
Cow::Owned(bytes.to_vec())
}
}
impl TiffValue for tags::PhotometricInterpretation {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::PlanarConfiguration {
const BYTE_LEN: u8 = 2;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::Predictor {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
impl TiffValue for tags::ResolutionUnit {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
1
}
fn data(&self) -> Cow<'_, [u8]> {
self.to_u16().to_ne_bytes().to_vec().into()
}
}
/// This is implemented for slices as the `SampleFormat` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html>
impl TiffValue for [tags::SampleFormat] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
/// This is implemented for slices as the `ExtraSamples` tag takes a count of `N`.
///
/// Use `core::slice::from_ref` if you really need to write a single element.
///
/// See: <https://web.archive.org/web/20191120220815/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html>
impl TiffValue for [tags::ExtraSamples] {
const BYTE_LEN: u8 = <u16 as TiffValue>::BYTE_LEN;
const FIELD_TYPE: Type = Type::SHORT;
fn count(&self) -> usize {
self.len()
}
fn data(&self) -> Cow<'_, [u8]> {
let mut buf: Vec<u8> = Vec::with_capacity(self.len() * 2);
for x in self {
buf.extend_from_slice(&x.to_u16().to_ne_bytes());
}
Cow::Owned(buf)
}
}
// If you pass `&Vec<_>` then you'd get at first sight the complaint:
//
// `Vec<T>` does not implement `TiffValue`
//
// and a list of implementations of `TiffValue` that are quite unrelated. That is not very helpful.
// We do not *want* `Vec` to implement the trait as an owning type, instead you should pass a
// reference to a slice. The error message is further customized in the diagnostics of the trait.
#[diagnostic::do_not_recommend]
impl<T: TiffValue + ?Sized> TiffValue for &'_ T { impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
const BYTE_LEN: u8 = T::BYTE_LEN; const BYTE_LEN: u8 = T::BYTE_LEN;
const FIELD_TYPE: Type = T::FIELD_TYPE; const FIELD_TYPE: Type = T::FIELD_TYPE;
@@ -467,6 +610,27 @@ impl<T: TiffValue + ?Sized> TiffValue for &'_ T {
} }
} }
/// An array is treated like a slice of the same length.
impl<T, const N: usize> TiffValue for [T; N]
where
[T]: TiffValue,
{
const BYTE_LEN: u8 = <[T]>::BYTE_LEN;
const FIELD_TYPE: Type = <[T]>::FIELD_TYPE;
fn count(&self) -> usize {
self.as_slice().count()
}
fn write<W: Write>(&self, writer: &mut TiffWriter<W>) -> TiffResult<()> {
self.as_slice().write(writer)
}
fn data(&self) -> Cow<'_, [u8]> {
self.as_slice().data()
}
}
macro_rules! impl_tiff_value_for_contiguous_sequence { macro_rules! impl_tiff_value_for_contiguous_sequence {
($inner_type:ty; $bytes:expr; $field_type:expr) => { ($inner_type:ty; $bytes:expr; $field_type:expr) => {
impl $crate::encoder::TiffValue for [$inner_type] { impl $crate::encoder::TiffValue for [$inner_type] {
+7
View File
@@ -5,6 +5,7 @@ use std::string;
use quick_error::quick_error; use quick_error::quick_error;
use crate::decoder::ChunkType; use crate::decoder::ChunkType;
use crate::tags::Type;
use crate::tags::{ use crate::tags::{
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag, CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
}; };
@@ -208,6 +209,12 @@ quick_error! {
ReconfiguredAfterImageWrite { ReconfiguredAfterImageWrite {
display("attempted to reconfigure the encoder after image writing has started") display("attempted to reconfigure the encoder after image writing has started")
} }
ByteOrderMismatch {
display("attempted to use data with a byte order that does not match the required one")
}
MismatchedEntryLength {ty: Type, found: usize } {
display("the length of data is not a multiple of the size of the type {ty:?}, found {found}")
}
} }
} }
+180 -1
View File
@@ -1,3 +1,5 @@
use crate::encoder::TiffValue;
macro_rules! tags { macro_rules! tags {
{ {
// Permit arbitrary meta items, which include documentation. // Permit arbitrary meta items, which include documentation.
@@ -10,8 +12,9 @@ macro_rules! tags {
$( #[$enum_attr] )* $( #[$enum_attr] )*
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[non_exhaustive] #[non_exhaustive]
#[repr($ty)]
pub enum $name { pub enum $name {
$($(#[$ident_attr])* $tag,)* $($(#[$ident_attr])* $tag = $val,)*
$( $(
#[$unknown_meta] #[$unknown_meta]
Unknown($ty), Unknown($ty),
@@ -335,6 +338,150 @@ pub enum ExtraSamples(u16) {
} }
} }
/// A value represented as in-memory bytes with flexible byteorder.
pub struct ValueBuffer {
/// The raw bytes of the value.
bytes: Vec<u8>,
/// The type of the value.
ty: Type,
/// The number of items, as `bytes` may be oversized while holding bytes that are initialized
/// but not used by any value.
count: u64,
/// The byte order of the value.
byte_order: ByteOrder,
}
impl ValueBuffer {
/// A value with a count of zero.
///
/// The byte order is set to the native byte order of the platform.
pub fn empty(ty: Type) -> Self {
ValueBuffer {
bytes: vec![],
ty,
count: 0,
byte_order: ByteOrder::native(),
}
}
/// Create a value with native byte order from in-memory data.
pub fn from_value<T: TiffValue>(value: &T) -> Self {
ValueBuffer {
bytes: value.data().into_owned(),
ty: <T as TiffValue>::FIELD_TYPE,
count: value.count() as u64,
byte_order: ByteOrder::native(),
}
}
pub fn byte_order(&self) -> ByteOrder {
self.byte_order
}
pub fn data_type(&self) -> Type {
self.ty
}
/// The count of items in the value.
pub fn count(&self) -> u64 {
debug_assert!({
self.ty
.value_bytes(self.count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.count
}
/// View the underlying raw bytes of this value.
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.assumed_len_from_count()]
}
/// View the underlying mutable raw bytes of this value.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
let len = self.assumed_len_from_count();
&mut self.bytes[..len]
}
/// See [`Value::into_ifd_pointer`](crate::decoder::ifd::Value::into_ifd_pointer).
pub(crate) fn as_ifd_pointer(&self) -> Option<IfdPointer> {
if self.count != 1 {
return None;
}
match self.ty {
// See [`ifd::Value::into_ifd_pointer`] there is enough legacy that uses `LONG` to
// refer to an offset into the file with the meaning of a directory pointer. That is
// rather annoying but legacy and not a big deal.
Type::IFD | Type::LONG => {
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[..4]);
ByteOrder::native().convert(Type::IFD, &mut buf, self.byte_order);
Some(IfdPointer(u32::from_ne_bytes(buf).into()))
}
Type::IFD8 => {
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[..8]);
ByteOrder::native().convert(Type::IFD8, &mut buf, self.byte_order);
Some(IfdPointer(u64::from_ne_bytes(buf)))
}
_ => None,
}
}
/// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count();
self.byte_order
.convert(self.ty, &mut self.bytes[..len], byte_order);
self.byte_order = byte_order;
}
/// Prepare the internal for a value `to_len` bytes long.
///
/// Shrinks the allocation if it is far too large or extends it if it is too small. In either
/// case ensures that at least `to_len` bytes are initialized for [`Self::raw_bytes_mut`].
pub(crate) fn prepare_length(&mut self, to_len: usize) {
if to_len > self.bytes.len() {
self.bytes.resize(to_len, 0);
}
if self.bytes.len() < to_len / 2 {
self.bytes.truncate(to_len);
self.bytes.shrink_to_fit();
}
}
/// Internal method to change the type and count while re-interpreting the byte buffer.
///
/// Should only be called after writing bytes to the internal buffer prepared with
/// `Self::prepare_length`.
pub(crate) fn assume_type(&mut self, ty: Type, count: u64, bo: ByteOrder) {
debug_assert!({
ty.value_bytes(count)
.is_ok_and(|n| n <= self.bytes.len() as u64)
});
self.byte_order = bo;
self.ty = ty;
self.count = count;
}
pub(crate) fn raw_bytes_mut(&mut self) -> &mut [u8] {
&mut self.bytes
}
fn assumed_len_from_count(&self) -> usize {
usize::from(self.ty.byte_len()) * self.count as usize
}
}
/// Byte order of the TIFF file. /// Byte order of the TIFF file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteOrder { pub enum ByteOrder {
@@ -410,3 +557,35 @@ pub(crate) enum EndianBytes {
Four, Four,
Eight, Eight,
} }
#[test]
fn convert_ifd_u32() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
#[test]
fn convert_ifd_long8() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
+37
View File
@@ -0,0 +1,37 @@
use tiff::{
decoder::Decoder,
encoder::{colortype::Gray8, EncodeMetadataOptions, TiffEncoder},
tags::Tag,
};
#[test]
fn copies_exif_data() {
let src = std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/images/predictor-3-gray-f32.tif"
));
let src = std::fs::File::open(src).unwrap();
let mut src = Decoder::new(src).unwrap();
let mut data = std::io::Cursor::new(vec![]);
{
let mut dst = TiffEncoder::new(&mut data).unwrap();
let mut img = dst.new_image::<Gray8>(1, 1).unwrap();
let report = img
.write_metadata_copy_from(&mut src, EncodeMetadataOptions::default())
.unwrap();
assert_eq!(report.copied, 8);
assert_eq!(report.filtered, 0, "There are no tags we do not recognize");
img.write_data(&[0x80]).unwrap();
}
data.set_position(0);
{
let mut roundtrip = Decoder::new(data).unwrap();
assert!(roundtrip.find_tag(Tag::DateTime).unwrap().is_some());
}
}