Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32dda6e11f | ||
|
|
74191f92bb | ||
|
|
f057b5f47d | ||
|
|
b3ced063ef | ||
|
|
76721c6893 | ||
|
|
348bbda906 |
No files matched your search
-28
@@ -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
@@ -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
@@ -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()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
@@ -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> {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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
@@ -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.
@@ -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