Compare commits

..
Author SHA1 Message Date
A. Molzer 418ea182c3 Release notes for 0.11.2 2026-01-25 00:33:33 +01:00
Aurelia Molzer 529fa64e13 Merge pull request #333 from image-rs/read-image-all-planes
Make `read_image_to_buffer` read out all planes
2026-01-24 23:28:07 +00:00
A. Molzer 3d6b94b65a Make read_image_to_buffer read out all planes 2026-01-24 21:24:55 +01:00
Aurelia Molzer a51b23091e Merge pull request #332 from image-rs/public-example-for-pam
Public ColorType::{num_samples, bit_depth}
2026-01-24 04:26:31 +00:00
A. Molzer ac65bd8477 Public ColorType::{num_samples, bit_depth}
This makes the decode example capable of writing correct PAM images in
many more cases, such as bilevel encoded (Fax3) masks.
2026-01-24 05:24:19 +01:00
10 changed files with 226 additions and 605 deletions

No files matched your search

+17
View File
@@ -1,3 +1,20 @@
# 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
Fixes:
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tiff"
version = "0.11.1"
version = "0.11.2"
edition = "2021"
resolver = "2"
+104 -1
View File
@@ -37,6 +37,7 @@ fn debug_planes(
(width, height): (u32, u32),
) {
let (depth, mut tupltype) = match colortype {
// Note: we will expand, so this is in fact not BLACKANDWHITE
tiff::ColorType::Gray(_) => (1, "GRAYSCALE"),
tiff::ColorType::RGB(_) => (3, "RGB"),
tiff::ColorType::RGBA(_) => (4, "RGB_ALPHA"),
@@ -49,8 +50,57 @@ 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) {
DecodingBuffer::U8(_) => u64::from(u8::MAX),
// `U8` is also our representation of bit-packed color. PAM uses the smallest number of
// *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::U32(_) => u64::from(u32::MAX),
DecodingBuffer::U64(_) => u64::MAX,
@@ -101,3 +151,56 @@ fn debug_planes(
.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()]);
}
}
}
+42 -8
View File
@@ -1396,14 +1396,16 @@ impl<R: Read + Seek> Decoder<R> {
/// # Bugs
///
/// 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`.
/// the first sample's plane. This will be fixed in a future major version of `tiff`. To read
/// multiple planes, [`Self::read_image_to_buffer`] can be used instead.
///
/// # Intent to deprecate
///
/// Use `DecodingResult::resize_to` and `read_image_bytes` instead where possible, preserving
/// the buffer across multiple calls. This old method will likely keep its bugged planar
/// behavior until it is fully replaced, to ensure that existing code will not run into
/// unexpectedly large allocations that will error on limits instead.
/// Use [`Self::read_image_to_buffer`] or a combination of [`DecodingResult::resize_to`] and
/// [`Self::read_image_bytes`] instead where possible, preserving the buffer across multiple
/// calls. This old method will likely keep its bugged planar behavior until it is fully
/// replaced, to ensure that existing code will not run into unexpectedly large allocations
/// that will error on limits instead.
pub fn read_image(&mut self) -> TiffResult<DecodingResult> {
let readout = self.image().readout_for_image()?;
@@ -1422,6 +1424,30 @@ impl<R: Read + Seek> Decoder<R> {
/// 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.
///
/// ## 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
///
/// ```
@@ -1443,12 +1469,20 @@ impl<R: Read + Seek> Decoder<R> {
result: &mut DecodingResult,
) -> TiffResult<BufferLayoutPreference> {
let readout = self.image().readout_for_image()?;
let planes = readout.to_plane_layout()?;
let layout = BufferLayoutPreference::from_planes(&planes);
let extent = readout.result_extent_for_planes(0..1)?;
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 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)?;
self.read_image_bytes(result.as_buffer(0).as_bytes_mut())?;
Ok(layout)
+10 -241
View File
@@ -2,25 +2,24 @@ pub use tiff_value::*;
use std::{
cmp,
io::{self, Read, Seek, Write},
io::{self, Seek, Write},
marker::PhantomData,
mem,
num::{NonZeroU64, TryFromIntError},
};
use crate::{
decoder::{ifd::Entry, Decoder},
decoder::ifd::Entry,
error::{TiffResult, UsageError},
tags::{
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
},
Directory, TiffError, TiffFormatError,
};
pub mod colortype;
pub mod compression;
mod tag_filter;
mod tiff_value;
mod writer;
@@ -41,8 +40,6 @@ pub type Predictor = crate::tags::Predictor;
#[cfg(feature = "deflate")]
pub type DeflateLevel = compression::DeflateLevel;
pub use tag_filter::TagFilter;
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Compression {
#[default]
@@ -437,148 +434,6 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
self.directory.extend(entries);
}
/// Copy metadata from the IFD that a decoder refers to.
///
/// You generally want to use [`ImageEncoder::write_metadata_copy_from`] which automatically
/// fills out the [`EncodeMetadataApplicability`] as applicable to the chosen color space and
/// other sample configuration.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
kind: EncodeMetadataApplicability,
) -> TiffResult<EncodeMetadataReport> {
let mut report = EncodeMetadataReport {
copied: 0,
filtered: 0,
};
enum Role {
Ifd,
ExifPrivate,
GpsPrivate,
}
let ptr = decoder.ifd_pointer().unwrap();
let mut stack = vec![(ptr, Role::Ifd, usize::MAX)];
let mut buf = ValueBuffer::empty(Type::UNDEFINED);
// Directories which we will later write in reverse-pre-order.
let mut pre_order_dirs: Vec<(Directory, usize, Tag)> = vec![];
while let Some((ifd, role, parent)) = stack.pop() {
let dir = decoder.read_directory(ifd)?;
let mut created_dir = Directory::empty();
let filter = match role {
Role::Ifd => TagFilter::filter_primary,
Role::ExifPrivate => TagFilter::filter_exif_private,
Role::GpsPrivate => TagFilter::filter_gps_private,
};
let mut ifd_reader = decoder.read_directory_tags(&dir);
for (tag, entry) in dir.iter() {
let level = filter(options.tag_filter, tag.to_u16());
match options
.tag_filter
.should_keep_for_image_writer(level, &kind)
{
tag_filter::Choice::Ok => {}
tag_filter::Choice::Descend(subifd_kind) => {
// Only single pointers can be directories to descend to, avoid an overly
// large read by pulling that value out of the file.
if entry.count() != 1 {
report.filtered = report.filtered.saturating_add(1);
continue;
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
// Should not happen (we're iterating the dict already), but be robust.
report.filtered = report.filtered.saturating_add(1);
continue;
};
let Some(ifd) = buf.as_ifd_pointer() else {
report.filtered = report.filtered.saturating_add(1);
continue;
};
let role = match subifd_kind {
tag_filter::SubifdKind::ExifPrivate => Role::ExifPrivate,
tag_filter::SubifdKind::Gps => Role::GpsPrivate,
tag_filter::SubifdKind::Interoperability => {
// Only applicable to compressed images.
report.filtered = report.filtered.saturating_add(1);
continue;
}
};
let this_as_parent = pre_order_dirs.len();
stack.push((ifd, role, this_as_parent));
}
tag_filter::Choice::Discard => continue,
tag_filter::Choice::Filtered
// Not supported yet.
| tag_filter::Choice::Special(tag_filter::Special::JpegThumbnail) => {
// Not to be copied.
report.filtered = report.filtered.saturating_add(1);
continue;
}
tag_filter::Choice::Special(tag_filter::Special::CompressedExclusive) => {
// We're not writing an App segment for a compressed file, are we?
continue;
}
}
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
report.filtered = report.filtered.saturating_add(1);
continue;
}
// Re-encode the value to the target file.
buf.set_byte_order(ByteOrder::native());
let entry = self.write_entry_buf(&buf)?;
created_dir.extend([(tag, entry)]);
report.copied = report.copied.saturating_add(1);
}
let tag_in_parent = match role {
// should be unreachable? For now. But thumbnails would use this so we best be a
// little cautious here. Also thumbnails are weird, they do have image data but we
// would not copy that data itself with these methods. Just eh.
//
// Anyways the tree root will use this tag as a dummy.
Role::Ifd => Tag::SubIfd,
Role::ExifPrivate => Tag::ExifDirectory,
Role::GpsPrivate => Tag::GpsDirectory,
};
pre_order_dirs.push((created_dir, parent, tag_in_parent));
}
// Now post-order traverse the directory tree.
while let Some((directory, parent, tag)) = pre_order_dirs.pop() {
// The root is the items for the IFD itself.
if pre_order_dirs.is_empty() {
// We do not enter ourselves into a parent directory.
let _ = (parent, tag);
self.extend_from(&directory);
break;
}
let offset = Self::write_directory_inner(self.writer, &directory)?;
let offset = K::convert_offset(offset)?;
let entry = self.write_entry(offset)?;
let (parent_dir, _, _) = &mut pre_order_dirs[parent];
parent_dir.extend([(tag, entry)]);
}
Ok(report)
}
/// Define the parent directory.
///
/// Each directory has an offset based link to its successor, forming a linked list in the
@@ -605,21 +460,17 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
}
fn write_directory(&mut self) -> TiffResult<u64> {
Self::write_directory_inner(self.writer, &self.directory)
}
fn write_directory_inner(writer: &mut TiffWriter<W>, directory: &Directory) -> TiffResult<u64> {
// Start by turning all buffered unwritten values into entries.
let offset = writer.offset();
K::write_entry_count(writer, directory.len())?;
let offset = self.writer.offset();
K::write_entry_count(self.writer, self.directory.len())?;
let offset_bytes = mem::size_of::<K::OffsetType>();
for (tag, entry) in directory.iter() {
writer.write_u16(tag.to_u16())?;
writer.write_u16(entry.field_type().to_u16())?;
for (tag, entry) in self.directory.iter() {
self.writer.write_u16(tag.to_u16())?;
self.writer.write_u16(entry.field_type().to_u16())?;
let count = K::convert_offset(entry.count())?;
count.write(writer)?;
writer.write_bytes(&entry.offset()[..offset_bytes])?;
count.write(self.writer)?;
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
}
Ok(offset)
@@ -731,72 +582,6 @@ impl<'a, W: Write + Seek, K: TiffKind> Drop for DirectoryEncoder<'a, W, K> {
}
}
#[non_exhaustive]
#[derive(Clone, Copy)]
pub struct EncodeMetadataOptions<'lt> {
/// Several metadata kinds contain additional tags in further, linked directories that are only
/// referenced with an `Ifd`/`Ifd8` tag. These directories may have their own semantics, e.g. a
/// different set of tags or even new value types.
///
/// Since we know that the whole directory contains only metadata it would be possible to copy
/// all tags contained in it, however tags that can not be semantically validated might contain
/// data that is only valid within the original image or must be transformed during the copy.
/// For instance, JPEG thumbnails are embedded with separate offset and length fields (`0x0201`
/// and `0x202`). (This has caused the author mental harm, please do not consult me about it).
///
/// Setting this flag will skip such unknown tags instead of attempting a potentially incorrect
/// copy. However the set of copied metadata is then dependent on the `tiff` library.
pub tag_filter: &'lt TagFilter,
// TODO:
// Some tags are not allowed to be recorded or can be semantically validated. The library can
// skip these tags while copying metadata. For instance, `YCbCrCoefficients` _may_ be
// recorded for images in `YCbCr` photometric interpretation but _must not_ be recorded for
// images in any other interpretation (colorspace conversion should have been done during the
// transformation).
// pub validate_tags: enum ValidateTags {
// None,
// Skip,
// Error,
// Strict /* Error but also do not allow anything that can not be validated */
// },
}
impl Default for EncodeMetadataOptions<'_> {
fn default() -> Self {
EncodeMetadataOptions {
tag_filter: TagFilter::only_known_tags(),
}
}
}
/// Describe the kind of image for which metadata is recorded.
///
/// Metadata that is not applicable to the color space or the layout may be skipped. For instance,
/// subsampling basis position is only relevant on YCbCr / Chroma color photometric interpretation
/// where subsampling is defined.
#[non_exhaustive]
#[derive(Clone, Copy, Default)]
pub struct EncodeMetadataApplicability {
/// The photometric interpretation of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// photometric interpretation are recorded.
pub photometric: Option<PhotometricInterpretation>,
/// The planar configuration of the image data.
///
/// If `None` then metadata tags are not filtered, otherwise only tags applicable to the given
/// configuration are recorded.
pub planar: Option<PlanarConfiguration>,
}
#[non_exhaustive]
pub struct EncodeMetadataReport {
/// Number of tags transferred (recursively).
pub copied: u64,
/// Number of tags encountered that were skipped due to the provided filter.
pub filtered: u64,
}
/// Type to encode images strip by strip.
///
/// You should call `finish` on this when you are finished with it.
@@ -1037,22 +822,6 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T,
Ok(())
}
/// Copy metadata from the IFD that a decoder refers to.
pub fn write_metadata_copy_from<R: Read + Seek>(
&mut self,
decoder: &mut Decoder<R>,
options: EncodeMetadataOptions<'_>,
) -> TiffResult<EncodeMetadataReport> {
self.encoder.write_metadata_copy_from(
decoder,
options,
EncodeMetadataApplicability {
photometric: Some(<T>::TIFF_VALUE),
planar: Some(PlanarConfiguration::Chunky),
},
)
}
/// Set image resolution
pub fn resolution(&mut self, unit: ResolutionUnit, value: Rational) {
self.encoder
-256
View File
@@ -1,256 +0,0 @@
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,
}
+4 -2
View File
@@ -51,7 +51,8 @@ pub enum ColorType {
Multiband { bit_depth: u8, num_samples: u16 },
}
impl ColorType {
fn bit_depth(&self) -> u8 {
/// The number of bits used to store each sample.
pub fn bit_depth(&self) -> u8 {
match *self {
ColorType::Gray(b)
| ColorType::RGB(b)
@@ -66,7 +67,8 @@ impl ColorType {
}
}
fn num_samples(&self) -> u16 {
/// The number of samples in each representation of a single color.
pub fn num_samples(&self) -> u16 {
match *self {
ColorType::Gray(_) => 1,
ColorType::RGB(_) => 3,
-58
View File
@@ -407,32 +407,6 @@ impl ValueBuffer {
&mut self.bytes[..len]
}
/// See [`Value::into_ifd_pointer`](crate::decoder::ifd::Value::into_ifd_pointer).
pub(crate) fn as_ifd_pointer(&self) -> Option<IfdPointer> {
if self.count != 1 {
return None;
}
match self.ty {
// See [`ifd::Value::into_ifd_pointer`] there is enough legacy that uses `LONG` to
// refer to an offset into the file with the meaning of a directory pointer. That is
// rather annoying but legacy and not a big deal.
Type::IFD | Type::LONG => {
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[..4]);
ByteOrder::native().convert(Type::IFD, &mut buf, self.byte_order);
Some(IfdPointer(u32::from_ne_bytes(buf).into()))
}
Type::IFD8 => {
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[..8]);
ByteOrder::native().convert(Type::IFD8, &mut buf, self.byte_order);
Some(IfdPointer(u64::from_ne_bytes(buf)))
}
_ => None,
}
}
/// Change the byte order of the value representation.
pub fn set_byte_order(&mut self, byte_order: ByteOrder) {
let len = self.assumed_len_from_count();
@@ -557,35 +531,3 @@ pub(crate) enum EndianBytes {
Four,
Eight,
}
#[test]
fn convert_ifd_u32() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
#[test]
fn convert_ifd_long8() {
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
value.set_byte_order(bo);
let ptr = value
.as_ifd_pointer()
.expect("Should be convertible to IfdPointer");
// Should be in host byte order again.
assert_eq!(ptr.0, 0x42);
}
}
+48 -1
View File
@@ -1,4 +1,4 @@
use tiff::decoder::{Decoder, TiffCodingUnit};
use tiff::decoder::{Decoder, DecodingResult, Limits, TiffCodingUnit};
use std::fs::File;
use std::path::PathBuf;
@@ -74,3 +74,50 @@ 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);
}
-37
View File
@@ -1,37 +0,0 @@
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());
}
}