Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32dda6e11f | ||
|
|
74191f92bb | ||
|
|
f057b5f47d | ||
|
|
b3ced063ef | ||
|
|
76721c6893 | ||
|
|
348bbda906 | ||
|
|
1bd6293857 | ||
|
|
87341f9075 | ||
|
|
43343f64cd | ||
|
|
be04326264 | ||
|
|
93a68f52f5 | ||
|
|
5f329840c8 | ||
|
|
1d138a5568 | ||
|
|
9a23090b6e | ||
|
|
9d409433e8 | ||
|
|
cf680424d9 | ||
|
|
d3b777c961 | ||
|
|
24b008a7c3 | ||
|
|
34bd1da99e | ||
|
|
cfff04c97d | ||
|
|
915cc20945 | ||
|
|
74b2857364 | ||
|
|
1dab89f10e | ||
|
|
45f617b9f5 | ||
|
|
170bd3697c | ||
|
|
855eb4ef23 | ||
|
|
fee650d7f6 | ||
|
|
ee8d13249e | ||
|
|
169a896851 | ||
|
|
082a35adb9 | ||
|
|
923df5f0bd | ||
|
|
c4328644b8 | ||
|
|
27bfcf5562 | ||
|
|
7d498a0284 | ||
|
|
5b21bcc75c |
No files matched your search
@@ -9,14 +9,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.74.0", stable, beta, nightly]
|
||||
rust: ["1.85.0", stable, beta, nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ matrix.rust == '1.74.0' }}
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
- name: Generate Cargo.lock with minimal-version dependencies
|
||||
if: ${{ matrix.rust == '1.74.0' }}
|
||||
if: ${{ matrix.rust == '1.85.0' }}
|
||||
run: cargo -Zminimal-versions generate-lockfile
|
||||
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: build
|
||||
run: cargo build -v
|
||||
- name: test
|
||||
if: ${{ matrix.rust != '1.74.0' }}
|
||||
if: ${{ matrix.rust != '1.85.0' }}
|
||||
run: cargo test -v && cargo doc -v
|
||||
|
||||
features:
|
||||
|
||||
+31
@@ -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
|
||||
|
||||
- `Directory` now implements `FromIterator<(Tag, Value)>`.
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "tiff"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# note: when changed, also update test runner in `.github/workflows/rust.yml`
|
||||
rust-version = "1.74.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
license = "MIT"
|
||||
description = "TIFF decoding and encoding library in pure Rust"
|
||||
@@ -14,7 +14,7 @@ authors = ["The image-rs Developers"]
|
||||
repository = "https://github.com/image-rs/image-tiff"
|
||||
categories = ["multimedia::images", "multimedia::encoding"]
|
||||
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*"]
|
||||
exclude = ["tests/images/*", "tests/fuzz_images/*", "tests/**/*.tif"]
|
||||
|
||||
[dependencies]
|
||||
half = { version = "2.4.1" }
|
||||
|
||||
+144
-12
@@ -6,12 +6,12 @@ use std::mem;
|
||||
use std::str;
|
||||
|
||||
use super::stream::{ByteOrder, EndianReader};
|
||||
use crate::tags::{IfdPointer, Tag, Type};
|
||||
use crate::tags::{IfdPointer, Tag, Type, ValueBuffer};
|
||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||
|
||||
use self::Value::{
|
||||
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig,
|
||||
Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig,
|
||||
Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, SRational, Short, Signed, SignedBig,
|
||||
SignedByte, SignedShort, Unsigned, UnsignedBig,
|
||||
};
|
||||
|
||||
#[allow(unused_qualifications)]
|
||||
@@ -30,8 +30,16 @@ pub enum Value {
|
||||
Double(f64),
|
||||
List(Vec<Value>),
|
||||
Rational(u32, u32),
|
||||
#[deprecated(
|
||||
note = "Not implemented in BigTIFF with a standard tag value",
|
||||
since = "0.11.1"
|
||||
)]
|
||||
RationalBig(u64, u64),
|
||||
SRational(i32, i32),
|
||||
#[deprecated(
|
||||
note = "Not implemented in BigTIFF with a standard tag value",
|
||||
since = "0.11.1"
|
||||
)]
|
||||
SRationalBig(i64, i64),
|
||||
Ascii(String),
|
||||
Ifd(u32),
|
||||
@@ -170,7 +178,8 @@ impl Value {
|
||||
Unsigned(val) => Ok(vec![val]),
|
||||
UnsignedBig(val) => Ok(vec![u32::try_from(val)?]),
|
||||
Rational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
RationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::RationalBig(numerator, denominator) => {
|
||||
Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?])
|
||||
}
|
||||
Ifd(val) => Ok(vec![val]),
|
||||
@@ -220,7 +229,8 @@ impl Value {
|
||||
new_vec.push(numerator);
|
||||
new_vec.push(denominator);
|
||||
}
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
new_vec.push(i32::try_from(numerator)?);
|
||||
new_vec.push(i32::try_from(denominator)?);
|
||||
}
|
||||
@@ -234,7 +244,8 @@ impl Value {
|
||||
Signed(val) => Ok(vec![val]),
|
||||
SignedBig(val) => Ok(vec![i32::try_from(val)?]),
|
||||
SRational(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?])
|
||||
}
|
||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
@@ -283,7 +294,8 @@ impl Value {
|
||||
Unsigned(val) => Ok(vec![val.into()]),
|
||||
UnsignedBig(val) => Ok(vec![val]),
|
||||
Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
||||
RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
#[expect(deprecated)]
|
||||
Value::RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
Ifd(val) => Ok(vec![val.into()]),
|
||||
IfdBig(val) => Ok(vec![val]),
|
||||
Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()),
|
||||
@@ -301,7 +313,8 @@ impl Value {
|
||||
new_vec.push(numerator.into());
|
||||
new_vec.push(denominator.into());
|
||||
}
|
||||
SRationalBig(numerator, denominator) => {
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => {
|
||||
new_vec.push(numerator);
|
||||
new_vec.push(denominator);
|
||||
}
|
||||
@@ -315,7 +328,8 @@ impl Value {
|
||||
Signed(val) => Ok(vec![val.into()]),
|
||||
SignedBig(val) => Ok(vec![val]),
|
||||
SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]),
|
||||
SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
#[expect(deprecated)]
|
||||
Value::SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]),
|
||||
_ => Err(TiffError::FormatError(TiffFormatError::InvalidTypeForTag)),
|
||||
}
|
||||
}
|
||||
@@ -565,7 +579,7 @@ 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)?;
|
||||
self.set_reader_offset_relative(bigtiff, reader, 0)?;
|
||||
|
||||
match self.type_ {
|
||||
Type::BYTE | Type::UNDEFINED => {
|
||||
@@ -723,6 +737,113 @@ impl Entry {
|
||||
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(
|
||||
value_count: u64,
|
||||
limits: &super::Limits,
|
||||
@@ -736,21 +857,32 @@ impl Entry {
|
||||
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,
|
||||
bo: ByteOrder,
|
||||
bigtiff: bool,
|
||||
reader: &mut EndianReader<R>,
|
||||
at: u64,
|
||||
) -> TiffResult<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let bo = reader.byte_order;
|
||||
|
||||
let offset = if bigtiff {
|
||||
self.offset_field_reader(bo).read_u64()?
|
||||
} else {
|
||||
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)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
+97
-1
@@ -5,7 +5,7 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use crate::tags::{
|
||||
CompressionMethod, IfdPointer, PhotometricInterpretation, PlanarConfiguration, Predictor,
|
||||
SampleFormat, Tag, Type,
|
||||
SampleFormat, Tag, Type, ValueBuffer,
|
||||
};
|
||||
use crate::{
|
||||
bytecast, ColorType, Directory, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError,
|
||||
@@ -1717,12 +1717,41 @@ impl<R: Seek + Read> ValueReader<R> {
|
||||
}
|
||||
|
||||
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.
|
||||
/// Return `Ok(None)` if the tag is not present.
|
||||
pub fn find_tag(&mut self, tag: Tag) -> TiffResult<Option<ifd::Value>> {
|
||||
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.
|
||||
pub fn find_tag_unsigned<T: TryFrom<u64>>(&mut self, tag: Tag) -> TiffResult<Option<T>> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::{tags::Tag, Directory};
|
||||
use crate::{TiffError, TiffFormatError, TiffResult};
|
||||
use crate::{
|
||||
tags::{Tag, ValueBuffer},
|
||||
Directory, TiffError, TiffFormatError, TiffResult,
|
||||
};
|
||||
|
||||
use super::ifd::{Entry, Value};
|
||||
use super::ValueReader;
|
||||
@@ -14,12 +16,25 @@ pub(crate) struct TagReader<'lt, R: EntryDecoder + ?Sized> {
|
||||
pub(crate) trait EntryDecoder {
|
||||
/// Turn an entry into a value by fetching the required bytes in the stream.
|
||||
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> {
|
||||
fn entry_val(&mut self, entry: &Entry) -> TiffResult<Value> {
|
||||
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> {
|
||||
@@ -32,6 +47,39 @@ impl<'a, R: EntryDecoder + ?Sized> TagReader<'a, R> {
|
||||
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> {
|
||||
match self.find_tag(tag)? {
|
||||
Some(val) => Ok(val),
|
||||
|
||||
+367
-23
@@ -2,24 +2,25 @@ pub use tiff_value::*;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
io::{self, Seek, Write},
|
||||
io::{self, Read, Seek, Write},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::{NonZeroU64, TryFromIntError},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decoder::ifd::Entry,
|
||||
decoder::{ifd::Entry, Decoder},
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{
|
||||
CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ResolutionUnit,
|
||||
SampleFormat, Tag, Type,
|
||||
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||
PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
pub mod colortype;
|
||||
pub mod compression;
|
||||
mod tag_filter;
|
||||
mod tiff_value;
|
||||
mod writer;
|
||||
|
||||
@@ -40,6 +41,8 @@ pub type Predictor = crate::tags::Predictor;
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateLevel = compression::DeflateLevel;
|
||||
|
||||
pub use tag_filter::TagFilter;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub enum Compression {
|
||||
#[default]
|
||||
@@ -301,12 +304,12 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
value.write(&mut writer)?;
|
||||
}
|
||||
|
||||
let entry = Self::write_value(
|
||||
let entry = Self::write_entry_inner(
|
||||
self.writer,
|
||||
&DirectoryEntry {
|
||||
DirectoryEntry {
|
||||
data_type: <T>::FIELD_TYPE,
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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> {
|
||||
let offset = self.writer.offset();
|
||||
value.write(self.writer)?;
|
||||
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.
|
||||
///
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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> {
|
||||
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.
|
||||
let offset = self.writer.offset();
|
||||
K::write_entry_count(self.writer, self.directory.len())?;
|
||||
let offset = writer.offset();
|
||||
K::write_entry_count(writer, directory.len())?;
|
||||
|
||||
let offset_bytes = mem::size_of::<K::OffsetType>();
|
||||
for (tag, entry) in self.directory.iter() {
|
||||
self.writer.write_u16(tag.to_u16())?;
|
||||
self.writer.write_u16(entry.field_type().to_u16())?;
|
||||
for (tag, entry) in directory.iter() {
|
||||
writer.write_u16(tag.to_u16())?;
|
||||
writer.write_u16(entry.field_type().to_u16())?;
|
||||
let count = K::convert_offset(entry.count())?;
|
||||
count.write(self.writer)?;
|
||||
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||
count.write(writer)?;
|
||||
writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||
}
|
||||
|
||||
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>,
|
||||
value: &DirectoryEntry<K::OffsetType>,
|
||||
value: DirectoryEntry<K::OffsetType>,
|
||||
) -> TiffResult<Entry> {
|
||||
let &DirectoryEntry {
|
||||
let DirectoryEntry {
|
||||
data: ref bytes,
|
||||
ref count,
|
||||
data_type,
|
||||
} = value;
|
||||
|
||||
let in_entry_bytes = mem::size_of::<K::OffsetType>();
|
||||
|
||||
let mut offset_bytes = [0; 8];
|
||||
|
||||
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.
|
||||
// Instead, write it to a buffer...
|
||||
let mut count_bytes = [0; 8];
|
||||
debug_assert!(in_entry_bytes == 4 || in_entry_bytes == 8);
|
||||
// Nominally Cow but we only expect `Cow::Borrowed`.
|
||||
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.
|
||||
fn last_written(&self) -> u64 {
|
||||
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.
|
||||
///
|
||||
/// 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::YResolution, Rational { n: 1, d: 1 })?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None.to_u16())?;
|
||||
encoder.write_tag(Tag::ResolutionUnit, ResolutionUnit::None)?;
|
||||
|
||||
Ok(ImageEncoder {
|
||||
encoder,
|
||||
@@ -709,6 +1037,22 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
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
|
||||
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
|
||||
self.encoder
|
||||
@@ -766,7 +1110,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
|
||||
} else {
|
||||
let mut sample_format: Vec<_> = <T>::BITS_PER_SAMPLE.to_vec();
|
||||
let replicated =
|
||||
core::iter::repeat(<T>::BITS_PER_SAMPLE[0]).take(self.extra_samples.len());
|
||||
core::iter::repeat_n(<T>::BITS_PER_SAMPLE[0], self.extra_samples.len());
|
||||
sample_format.extend(replicated);
|
||||
|
||||
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.
|
||||
.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
|
||||
.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,
|
||||
count: S,
|
||||
data: Vec<u8>,
|
||||
data: std::borrow::Cow<'data, [u8]>,
|
||||
}
|
||||
|
||||
/// Trait to abstract over Tiff/BigTiff differences.
|
||||
|
||||
@@ -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
@@ -1,14 +1,27 @@
|
||||
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;
|
||||
|
||||
/// Trait for types that can be encoded in a tiff file
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "the type `{Self}` does not implement `TiffValue`",
|
||||
note = "the trait is implemented for primitive types (`u8`, `i16`, `f32`, etc.)",
|
||||
note = "the trait is implemented for shared references to values",
|
||||
note = "the trait is implemented for slices, pass them by reference (e.g. `&[u8]`)",
|
||||
note = "the trait is implemented for arrays if it is implemented for a slice",
|
||||
note = "values in a `Vec` or `Box` should be dereferenced, e.g. `&vec[..]`"
|
||||
)]
|
||||
pub trait TiffValue {
|
||||
const BYTE_LEN: u8;
|
||||
const FIELD_TYPE: Type;
|
||||
|
||||
// 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 bytes(&self) -> usize {
|
||||
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 {
|
||||
const BYTE_LEN: u8 = T::BYTE_LEN;
|
||||
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 {
|
||||
($inner_type:ty; $bytes:expr; $field_type:expr) => {
|
||||
impl $crate::encoder::TiffValue for [$inner_type] {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::string;
|
||||
use quick_error::quick_error;
|
||||
|
||||
use crate::decoder::ChunkType;
|
||||
use crate::tags::Type;
|
||||
use crate::tags::{
|
||||
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, SampleFormat, Tag,
|
||||
};
|
||||
@@ -208,6 +209,12 @@ quick_error! {
|
||||
ReconfiguredAfterImageWrite {
|
||||
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
@@ -1,3 +1,5 @@
|
||||
use crate::encoder::TiffValue;
|
||||
|
||||
macro_rules! tags {
|
||||
{
|
||||
// Permit arbitrary meta items, which include documentation.
|
||||
@@ -10,8 +12,9 @@ macro_rules! tags {
|
||||
$( #[$enum_attr] )*
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
#[non_exhaustive]
|
||||
#[repr($ty)]
|
||||
pub enum $name {
|
||||
$($(#[$ident_attr])* $tag,)*
|
||||
$($(#[$ident_attr])* $tag = $val,)*
|
||||
$(
|
||||
#[$unknown_meta]
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ByteOrder {
|
||||
@@ -410,3 +557,35 @@ pub(crate) enum EndianBytes {
|
||||
Four,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user