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
17 changed files with 609 additions and 481 deletions

No files matched your search

-28
View File
@@ -1,31 +1,3 @@
# Version 0.11.3
Additions:
- Added support for `webp` compression (as per GDAL) under the `webp` feature flag (default: off).
- Added `Decoder::image_ifd`, creating an `IfdDecoder` of the current IFD.
- Added `IfdDecoder::directory`, returning the `Directory` underlying it.
- Added `LowerHex` and `UpwerHex` implementations for `IfdPointer`.
Notes:
- A new example `tiff-ls` demonstrates use of the new features.
# Version 0.11.2
Changes:
- Planar files can now be read with `Decoder::read_image_to_buffer` if the
limits allow for such a large buffer size. Previously only the first plane
was read and the decoder will attempt a fallback to this behavior if the
limits do not allow all planes to be allocated.
Fixes:
- An off-by-one error in a `debug_assert` of `ValueBuffer` caused its use in
`IfdDecoder::find_tag_buf` to panic outside release mode when the buffer
exactly matched the required size.
Additions:
- The methods `ColorType::{num_samples, bit_depth}` are now public. This
promises that we only have homogeneous color depths in this major version.
# Version 0.11.1 # Version 0.11.1
Fixes: Fixes:
+1 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tiff" name = "tiff"
version = "0.11.3" version = "0.11.1"
edition = "2021" edition = "2021"
resolver = "2" resolver = "2"
@@ -26,7 +26,6 @@ flate2 = { version = "1.0.20", optional = true }
weezl = { version = "0.1.10", optional = true } weezl = { version = "0.1.10", optional = true }
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }
zune-jpeg = { version = "0.5.1", optional = true } zune-jpeg = { version = "0.5.1", optional = true }
image-webp = {version = "0.2.4", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3.1" criterion = "0.3.1"
@@ -41,7 +40,6 @@ deflate = ["dep:flate2"]
fax = ["dep:fax34"] fax = ["dep:fax34"]
jpeg = ["dep:zune-jpeg"] jpeg = ["dep:zune-jpeg"]
lzw = ["dep:weezl"] lzw = ["dep:weezl"]
webp = ["dep:image-webp"]
zstd = ["dep:zstd"] zstd = ["dep:zstd"]
[[bench]] [[bench]]
+1 -104
View File
@@ -37,7 +37,6 @@ fn debug_planes(
(width, height): (u32, u32), (width, height): (u32, u32),
) { ) {
let (depth, mut tupltype) = match colortype { let (depth, mut tupltype) = match colortype {
// Note: we will expand, so this is in fact not BLACKANDWHITE
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"), tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
tiff::ColorType::RGB(_) => (3, "RGB"), tiff::ColorType::RGB(_) => (3, "RGB"),
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"), tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
@@ -50,57 +49,8 @@ fn debug_planes(
} }
}; };
// Number of samples in a line and number of sample lines in the image, depending on whether
// they are planar or not. This determines the layout of the image buffer when expanding bit
// packed samples to a full byte (we support power-of-two sample sizes, positive and negative,
// but only those).
let (swidth, sheight) = if layout.planes > 1 {
(
u64::from(width) * u64::from(colortype.num_samples()),
u64::from(height),
)
} else {
(
u64::from(width),
u64::from(height) * u64::from(colortype.num_samples()),
)
};
let mut fallback_buffer;
let maxval = match data.as_buffer(0) { let maxval = match data.as_buffer(0) {
// `U8` is also our representation of bit-packed color. PAM uses the smallest number of DecodingBuffer::U8(_) => u64::from(u8::MAX),
// *bytes* needed to represent a sample but we must also adhere to maxval. We could explode
// them here (e.g. represent 1-bit as `{0, 1}`) but we'd also set `maxval` accordingly and
// this requires tupltype to be `BLACKANDWHITE` so introducing a bunch of complexity. If
// you want to do that in your real code, go ahead.
DecodingBuffer::U8(buf) => match colortype.bit_depth() {
8 => u64::from(u8::MAX),
4 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_4bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
2 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_2bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
1 => {
fallback_buffer = vec![0; (swidth * sheight) as usize];
expand_1bit(buf, swidth, &mut fallback_buffer);
*data = DecodingResult::U8(fallback_buffer);
u64::from(u8::MAX)
}
_ => {
eprintln!(
"Unsupported bit depth for U8 buffer: {}",
colortype.bit_depth()
);
return;
}
},
DecodingBuffer::U16(_) => u64::from(u16::MAX), DecodingBuffer::U16(_) => u64::from(u16::MAX),
DecodingBuffer::U32(_) => u64::from(u32::MAX), DecodingBuffer::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX, DecodingBuffer::U64(_) => u64::MAX,
@@ -151,56 +101,3 @@ fn debug_planes(
.expect("Failed to write PAM data"); .expect("Failed to write PAM data");
} }
} }
/// Put exactly the ith `SCALE`-bit chunk of `val` into the least significant bits.
#[inline(always)]
fn bits_extract<const SCALE: u8>(val: u8, i: u8) -> u8 {
// Shift left to discard high bits, then right to bring desired bits down. This way also
// conveniently avoids all cases of overflow.
(val << (i * SCALE)) >> (8 - SCALE)
}
fn expand_1bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(8);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(8).zip(from_row) {
let data =
core::array::from_fn::<u8, 8, _>(|i| 0xff * bits_extract::<1>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
fn expand_2bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(4);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(4).zip(from_row) {
let data =
core::array::from_fn::<u8, 4, _>(|i| 0xaa * bits_extract::<2>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
fn expand_4bit(from: &[u8], samples_per_padded: u64, into: &mut [u8]) {
let stride = samples_per_padded.div_ceil(2);
let from_row = from.chunks_exact(stride as usize);
let into_row = into.chunks_exact_mut(samples_per_padded as usize);
for (from_row, into_row) in from_row.zip(into_row) {
for (into, &from) in into_row.chunks_mut(2).zip(from_row) {
let data =
core::array::from_fn::<u8, 2, _>(|i| 0x88 * bits_extract::<4>(from, i as u8));
into.copy_from_slice(&data[..into.len()]);
}
}
}
-124
View File
@@ -1,124 +0,0 @@
use std::borrow::Cow;
use tiff::decoder::Decoder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(image) = std::env::args_os().nth(1) else {
eprintln!("Usage: decode FILE");
return Ok(());
};
let file = std::fs::File::open(image)?;
let io = std::io::BufReader::new(file);
let mut reader = Decoder::new(io)?;
loop {
ls_dir(&mut reader);
if !reader.more_images() {
break;
}
}
Ok(())
}
fn ls_dir<R: std::io::Read + std::io::Seek>(reader: &mut Decoder<R>) {
if let Some(ifd) = reader.ifd_pointer() {
println!("Directory at {ifd:x}");
}
println!("Name\tHex\tType\tCount");
let ifd = reader.image_ifd();
for (tag, entry) in ifd.directory().iter() {
let name: &'static str = match tag {
tiff::tags::Tag::Artist => "Artist",
tiff::tags::Tag::BitsPerSample => "BitsPerSample",
tiff::tags::Tag::CellLength => "CellLength",
tiff::tags::Tag::CellWidth => "CellWidth",
tiff::tags::Tag::ColorMap => "ColorMap",
tiff::tags::Tag::Compression => "Compression",
tiff::tags::Tag::DateTime => "DateTime",
tiff::tags::Tag::ExtraSamples => "ExtraSamples",
tiff::tags::Tag::FillOrder => "FillOrder",
tiff::tags::Tag::FreeByteCounts => "FreeByteCounts",
tiff::tags::Tag::FreeOffsets => "FreeOffsets",
tiff::tags::Tag::GrayResponseCurve => "GrayResponseCurve",
tiff::tags::Tag::GrayResponseUnit => "GrayResponseUnit",
tiff::tags::Tag::HostComputer => "HostComputer",
tiff::tags::Tag::ImageDescription => "ImageDescription",
tiff::tags::Tag::ImageLength => "ImageLength",
tiff::tags::Tag::ImageWidth => "ImageWidth",
tiff::tags::Tag::Make => "Make",
tiff::tags::Tag::MaxSampleValue => "MaxSampleValue",
tiff::tags::Tag::MinSampleValue => "MinSampleValue",
tiff::tags::Tag::Model => "Model",
tiff::tags::Tag::NewSubfileType => "NewSubfileType",
tiff::tags::Tag::Orientation => "Orientation",
tiff::tags::Tag::PhotometricInterpretation => "PhotometricInterpretation",
tiff::tags::Tag::PlanarConfiguration => "PlanarConfiguration",
tiff::tags::Tag::ResolutionUnit => "ResolutionUnit",
tiff::tags::Tag::RowsPerStrip => "RowsPerStrip",
tiff::tags::Tag::SamplesPerPixel => "SamplesPerPixel",
tiff::tags::Tag::Software => "Software",
tiff::tags::Tag::StripByteCounts => "StripByteCounts",
tiff::tags::Tag::StripOffsets => "StripOffsets",
tiff::tags::Tag::SubfileType => "SubfileType",
tiff::tags::Tag::Threshholding => "Threshholding",
tiff::tags::Tag::XResolution => "XResolution",
tiff::tags::Tag::YResolution => "YResolution",
tiff::tags::Tag::Predictor => "Predictor",
tiff::tags::Tag::TileWidth => "TileWidth",
tiff::tags::Tag::TileLength => "TileLength",
tiff::tags::Tag::TileOffsets => "TileOffsets",
tiff::tags::Tag::TileByteCounts => "TileByteCounts",
tiff::tags::Tag::SubIfd => "SubIfd",
tiff::tags::Tag::SampleFormat => "SampleFormat",
tiff::tags::Tag::SMinSampleValue => "SMinSampleValue",
tiff::tags::Tag::SMaxSampleValue => "SMaxSampleValue",
tiff::tags::Tag::JPEGTables => "JPEGTables",
tiff::tags::Tag::ChromaSubsampling => "ChromaSubsampling",
tiff::tags::Tag::ChromaPositioning => "ChromaPositioning",
tiff::tags::Tag::ModelPixelScaleTag => "ModelPixelScaleTag",
tiff::tags::Tag::ModelTransformationTag => "ModelTransformationTag",
tiff::tags::Tag::ModelTiepointTag => "ModelTiepointTag",
tiff::tags::Tag::Copyright => "Copyright",
tiff::tags::Tag::ExifDirectory => "ExifDirectory",
tiff::tags::Tag::GpsDirectory => "GpsDirectory",
tiff::tags::Tag::IccProfile => "IccProfile",
tiff::tags::Tag::GeoKeyDirectoryTag => "GeoKeyDirectoryTag",
tiff::tags::Tag::GeoDoubleParamsTag => "GeoDoubleParamsTag",
tiff::tags::Tag::GeoAsciiParamsTag => "GeoAsciiParamsTag",
tiff::tags::Tag::ExifVersion => "ExifVersion",
tiff::tags::Tag::GdalNodata => "GdalNodata",
_ => "<unknown>",
};
let ty: Cow<'static, str> = match entry.field_type() {
tiff::tags::Type::BYTE => "u8".into(),
tiff::tags::Type::ASCII => "ascii".into(),
tiff::tags::Type::SHORT => "u16".into(),
tiff::tags::Type::LONG => "u32".into(),
tiff::tags::Type::RATIONAL => "r32".into(),
tiff::tags::Type::SBYTE => "i8".into(),
tiff::tags::Type::UNDEFINED => "byte".into(),
tiff::tags::Type::SSHORT => "s16".into(),
tiff::tags::Type::SLONG => "s32".into(),
tiff::tags::Type::SRATIONAL => "sr32".into(),
tiff::tags::Type::FLOAT => "f32".into(),
tiff::tags::Type::DOUBLE => "f64".into(),
tiff::tags::Type::IFD => "ifd32".into(),
tiff::tags::Type::LONG8 => "u64".into(),
tiff::tags::Type::SLONG8 => "i64".into(),
tiff::tags::Type::IFD8 => "ifd64".into(),
other => format!("{:x}", other.to_u16()).into(),
};
eprintln!(
"{name:16}\t{tag:4x}\t{ty}\t{count}",
tag = tag.to_u16(),
count = entry.count(),
);
}
}
+2 -11
View File
@@ -526,14 +526,13 @@ impl Image {
} }
} }
fn create_reader<'r, R: 'r + Read + Seek>( fn create_reader<'r, R: 'r + Read>(
reader: R, reader: R,
compression_method: CompressionMethod, compression_method: CompressionMethod,
compressed_length: u64, compressed_length: u64,
// FIXME: these should be `expect` attributes or we choose another way of passing them. // FIXME: these should be `expect` attributes or we choose another way of passing them.
#[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>, #[cfg_attr(not(feature = "jpeg"), allow(unused_variables))] jpeg_tables: Option<&[u8]>,
#[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32), #[cfg_attr(not(feature = "fax"), allow(unused_variables))] dimensions: (u32, u32),
#[cfg_attr(not(feature = "webp"), allow(unused_variables))] samples: u16,
) -> TiffResult<Box<dyn Read + 'r>> { ) -> TiffResult<Box<dyn Read + 'r>> {
Ok(match compression_method { Ok(match compression_method {
CompressionMethod::None => Box::new(reader), CompressionMethod::None => Box::new(reader),
@@ -608,13 +607,6 @@ impl Image {
reader, reader,
compressed_length, compressed_length,
)?), )?),
#[cfg(feature = "webp")]
CompressionMethod::WebP => Box::new(super::stream::WebPReader::new(
reader,
compressed_length,
samples,
)?),
method => { method => {
return Err(TiffError::UnsupportedError( return Err(TiffError::UnsupportedError(
TiffUnsupportedError::UnsupportedCompressionMethod(method), TiffUnsupportedError::UnsupportedCompressionMethod(method),
@@ -813,7 +805,7 @@ impl Image {
pub(crate) fn expand_chunk( pub(crate) fn expand_chunk(
&self, &self,
reader: &mut ValueReader<impl Read + Seek>, reader: &mut ValueReader<impl Read>,
buf: &mut [u8], buf: &mut [u8],
layout: &ReadoutLayout, layout: &ReadoutLayout,
chunk_index: u32, chunk_index: u32,
@@ -927,7 +919,6 @@ impl Image {
*compressed_bytes, *compressed_bytes,
self.jpeg_tables.as_deref().map(|a| &**a), self.jpeg_tables.as_deref().map(|a| &**a),
chunk_dims, chunk_dims,
self.samples,
)?; )?;
if is_output_chunk_rows && is_all_bits { if is_output_chunk_rows && is_all_bits {
+9 -48
View File
@@ -1396,16 +1396,14 @@ impl<R: Read + Seek> Decoder<R> {
/// # Bugs /// # Bugs
/// ///
/// When the image is stored as a planar configuration, this method will currently only read /// When the image is stored as a planar configuration, this method will currently only read
/// the first sample's plane. This will be fixed in a future major version of `tiff`. To read /// the first sample's plane. This will be fixed in a future major version of `tiff`.
/// multiple planes, [`Self::read_image_to_buffer`] can be used instead.
/// ///
/// # Intent to deprecate /// # Intent to deprecate
/// ///
/// Use [`Self::read_image_to_buffer`] or a combination of [`DecodingResult::resize_to`] and /// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving
/// [`Self::read_image_bytes`] instead where possible, preserving the buffer across multiple /// the buffer across multiple calls. This old method will likely keep its bugged planar
/// calls. This old method will likely keep its bugged planar behavior until it is fully /// behavior until it is fully replaced, to ensure that existing code will not run into
/// replaced, to ensure that existing code will not run into unexpectedly large allocations /// unexpectedly large allocations that will error on limits instead.
/// that will error on limits instead.
pub fn read_image(&mut self) -> TiffResult<DecodingResult> { pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let readout = self.image().readout_for_image()?; let readout = self.image().readout_for_image()?;
@@ -1424,30 +1422,6 @@ impl<R: Read + Seek> Decoder<R> {
/// configured limits, and then pass it to the underlying method. This is essentially a /// configured limits, and then pass it to the underlying method. This is essentially a
/// type-safe wrapper around the raw [`Self::read_image_bytes`] method. /// type-safe wrapper around the raw [`Self::read_image_bytes`] method.
/// ///
/// ## Planar behavior
///
/// If the image is stored as a planar configuration, an attempt is made to resize the buffer
/// to hold all planes. If that does not fit then only the first plane is read. Check the
/// buffer size against [`BufferLayoutPreference::complete_len`] to ensure that all planes were
/// read:
///
/// ```
/// use tiff::decoder::{Decoder, DecodingResult};
/// let mut result = DecodingResult::U8(vec![]);
///
/// let mut reader = /* */
/// # Decoder::new(std::io::Cursor::new(include_bytes!(concat!(
/// # env!("CARGO_MANIFEST_DIR"), "/tests/images/tiled-gray-i1.tif"
/// # )))).unwrap();
/// let layout = reader.read_image_to_buffer(&mut result)?;
///
/// if result.as_buffer(0).as_bytes().len() < layout.complete_len {
/// println!("Only the first plane was read");
/// }
///
/// # Ok::<_, tiff::TiffError>(())
/// ```
///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
@@ -1469,20 +1443,12 @@ impl<R: Read + Seek> Decoder<R> {
result: &mut DecodingResult, result: &mut DecodingResult,
) -> TiffResult<BufferLayoutPreference> { ) -> TiffResult<BufferLayoutPreference> {
let readout = self.image().readout_for_image()?; let readout = self.image().readout_for_image()?;
let planes = readout.to_plane_layout()?; let planes = readout.to_plane_layout()?;
let num_planes = if planes.total_bytes <= self.value_reader.limits.decoding_buffer_size {
planes.plane_offsets.len() as u16
} else {
1
};
let layout = BufferLayoutPreference::from_planes(&planes); let layout = BufferLayoutPreference::from_planes(&planes);
let extent = readout.result_extent_for_planes(0..num_planes)?;
// Compatibility: if this extent is too large our configured limits we will fall back to
// reading only the first plane.
result.resize_to_extent(extent, &self.value_reader.limits)?;
let extent = readout.result_extent_for_planes(0..1)?;
result.resize_to_extent(extent, &self.value_reader.limits)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?; self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
Ok(layout) Ok(layout)
@@ -1538,7 +1504,7 @@ impl<R: Read + Seek> Decoder<R> {
} }
/// Get the IFD decoder for our current image IFD. /// Get the IFD decoder for our current image IFD.
pub fn image_ifd(&mut self) -> IfdDecoder<'_> { fn image_ifd(&mut self) -> IfdDecoder<'_> {
IfdDecoder { IfdDecoder {
inner: tag_reader::TagReader { inner: tag_reader::TagReader {
decoder: &mut self.value_reader, decoder: &mut self.value_reader,
@@ -1885,11 +1851,6 @@ impl IfdDecoder<'_> {
pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> { pub fn get_tag_ascii_string(&mut self, tag: Tag) -> TiffResult<String> {
self.get_tag(tag)?.into_string() self.get_tag(tag)?.into_string()
} }
/// Inspect the raw underlying directory.
pub fn directory(&self) -> &Directory {
self.inner.ifd
}
} }
impl<'l> IfdDecoder<'l> { impl<'l> IfdDecoder<'l> {
-57
View File
@@ -1,6 +1,4 @@
//! All IO functionality needed for TIFF decoding //! All IO functionality needed for TIFF decoding
#[cfg(feature = "webp")]
use std::io::Cursor;
use std::io::{self, BufRead, BufReader, Read, Seek, Take}; use std::io::{self, BufRead, BufReader, Read, Seek, Take};
pub use crate::tags::ByteOrder; pub use crate::tags::ByteOrder;
@@ -356,61 +354,6 @@ impl<R: Read> Read for Group4Reader<R> {
} }
} }
#[cfg(feature = "webp")]
pub struct WebPReader {
inner: Cursor<Vec<u8>>,
}
#[cfg(feature = "webp")]
impl WebPReader {
pub fn new<R: Read + Seek>(
reader: R,
compressed_length: u64,
samples: u16,
) -> crate::TiffResult<Self> {
let mut decoder =
image_webp::WebPDecoder::new(io::BufReader::new(reader.take(compressed_length)))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if !(samples == 4 || (samples == 3 && !decoder.has_alpha())) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"bad sample count for WebP compressed data",
)
.into());
}
let total_bytes =
samples as usize * decoder.dimensions().0 as usize * decoder.dimensions().1 as usize;
let mut data = vec![0; total_bytes];
decoder
.read_image(&mut data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
// Add a fully opaque alpha channel if needed
if samples == 4 && !decoder.has_alpha() {
for i in (0..(total_bytes / 4)).rev() {
data[i * 4 + 3] = 255;
data[i * 4 + 2] = data[i * 3 + 2];
data[i * 4 + 1] = data[i * 3 + 1];
data[i * 4] = data[i * 3];
}
}
Ok(Self {
inner: Cursor::new(data),
})
}
}
#[cfg(feature = "webp")]
impl Read for WebPReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+241 -10
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::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer, 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]
@@ -434,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
@@ -460,17 +605,21 @@ 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)
@@ -582,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.
@@ -822,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
+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,
}
+2 -4
View File
@@ -51,8 +51,7 @@ pub enum ColorType {
Multiband { bit_depth: u8, num_samples: u16 }, Multiband { bit_depth: u8, num_samples: u16 },
} }
impl ColorType { impl ColorType {
/// The number of bits used to store each sample. fn bit_depth(&self) -> u8 {
pub fn bit_depth(&self) -> u8 {
match *self { match *self {
ColorType::Gray(b) ColorType::Gray(b)
| ColorType::RGB(b) | ColorType::RGB(b)
@@ -67,8 +66,7 @@ impl ColorType {
} }
} }
/// The number of samples in each representation of a single color. fn num_samples(&self) -> u16 {
pub fn num_samples(&self) -> u16 {
match *self { match *self {
ColorType::Gray(_) => 1, ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3, ColorType::RGB(_) => 3,
+58 -16
View File
@@ -1,5 +1,4 @@
use crate::encoder::TiffValue; use crate::encoder::TiffValue;
use core::fmt;
macro_rules! tags { macro_rules! tags {
{ {
@@ -170,18 +169,6 @@ pub enum Tag(u16) unknown(
// practice it just returns garbage and the validity does not matter greatly to us). // practice it just returns garbage and the validity does not matter greatly to us).
pub struct IfdPointer(pub u64); pub struct IfdPointer(pub u64);
impl fmt::LowerHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl core::fmt::UpperHex for IfdPointer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::UpperHex::fmt(&self.0, f)
}
}
tags! { tags! {
/// The type of an IFD entry (a 2 byte field). /// The type of an IFD entry (a 2 byte field).
pub enum Type(u16) { pub enum Type(u16) {
@@ -280,9 +267,6 @@ pub enum CompressionMethod(u16) unknown(
// Self-assigned by libtiff // Self-assigned by libtiff
ZSTD = 0xC350, ZSTD = 0xC350,
// Self-assigned by libtiff
WebP = 0xC351,
} }
} }
@@ -423,6 +407,32 @@ impl ValueBuffer {
&mut self.bytes[..len] &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. /// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) { pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count(); let len = self.assumed_len_from_count();
@@ -547,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);
}
}
-4
View File
@@ -2,7 +2,3 @@ subsubifd.tif:
url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download url: https://data.kitware.com/api/v1/file/hashsum/sha512/372ca32735c8a8fdbe2286dabead9e779da63f10ba81eda84625e5273f76d74ca1a47a978f67e9c00c12f7f72009c7b2c07a641e643bb0c463812f4ae7f15d6e/download
credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac credit: https://github.com/DigitalSlideArchive/tifftools/commit/4d00c70dbce828262f86c1431f7c66b1748965ac
license: CC0 license: CC0
usda/*.tif
url: https://registry.opendata.aws/naip/index.html
credit: NAIP on AWS was accessed on 2026-01-28 from https://registry.opendata.aws/naip.
license: Public Domain
+1 -24
View File
@@ -7,7 +7,7 @@ use tiff::ColorType;
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
const TEST_IMAGE_DIR: &str = "./tests/geotiff"; const TEST_IMAGE_DIR: &str = "./tests/images";
#[test] #[test]
fn test_geo_tiff() { fn test_geo_tiff() {
@@ -61,26 +61,3 @@ fn test_geo_tiff() {
assert_eq!(data.len(), 500); assert_eq!(data.len(), 500);
} }
} }
#[cfg(feature = "webp")]
#[test]
fn test_webp_tiff() {
let filenames = ["usda_naip_256_webp_z3.tif"];
let mut buffer = DecodingResult::U8(vec![]);
for filename in filenames.iter() {
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
let img_file = File::open(path).expect("Cannot find test image!");
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
loop {
decoder.read_image_to_buffer(&mut buffer).unwrap();
if !decoder.more_images() {
break;
}
decoder.next_image().unwrap();
}
}
}
+1 -48
View File
@@ -1,4 +1,4 @@
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit}; use tiff::decoder::{Decoder, TiffCodingUnit};
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
@@ -74,50 +74,3 @@ fn read_first_coding_unit() {
} }
} }
} }
#[test]
fn read_planes_by_limits() {
let path = PathBuf::from(TEST_IMAGE_DIR).join("planar-rgb-u8.tif");
let file = File::open(path).unwrap();
let io = std::io::BufReader::new(file);
let mut limits = Limits::default();
let mut buffer = DecodingResult::U8(vec![]);
// Make room for 1 plane.
let mut tif = Decoder::new(io).unwrap();
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 2 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 2;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499);
// Make room for 3 planes.
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 3;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
// Make room for 4 planes?
tif = tif.with_limits({
limits.decoding_buffer_size = 374 * 499 * 4;
limits.clone()
});
tif.read_image_to_buffer(&mut buffer).unwrap();
assert_eq!(buffer.as_buffer(0).byte_len(), 374 * 499 * 3);
}
Binary file not shown.
File renamed without changes.
+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());
}
}