Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32dda6e11f | ||
|
|
74191f92bb | ||
|
|
f057b5f47d | ||
|
|
b3ced063ef | ||
|
|
76721c6893 | ||
|
|
348bbda906 |
No files matched your search
+241
-10
@@ -2,24 +2,25 @@ pub use tiff_value::*;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
io::{self, Seek, Write},
|
||||
io::{self, Read, Seek, Write},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
num::{NonZeroU64, TryFromIntError},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decoder::ifd::Entry,
|
||||
decoder::{ifd::Entry, Decoder},
|
||||
error::{TiffResult, UsageError},
|
||||
tags::{
|
||||
ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation,
|
||||
ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
PlanarConfiguration, ResolutionUnit, SampleFormat, Tag, Type, ValueBuffer,
|
||||
},
|
||||
Directory, TiffError, TiffFormatError,
|
||||
};
|
||||
|
||||
pub mod colortype;
|
||||
pub mod compression;
|
||||
mod tag_filter;
|
||||
mod tiff_value;
|
||||
mod writer;
|
||||
|
||||
@@ -40,6 +41,8 @@ pub type Predictor = crate::tags::Predictor;
|
||||
#[cfg(feature = "deflate")]
|
||||
pub type DeflateLevel = compression::DeflateLevel;
|
||||
|
||||
pub use tag_filter::TagFilter;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub enum Compression {
|
||||
#[default]
|
||||
@@ -434,6 +437,148 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
self.directory.extend(entries);
|
||||
}
|
||||
|
||||
/// Copy metadata from the IFD that a decoder refers to.
|
||||
///
|
||||
/// You generally want to use [`ImageEncoder::write_metadata_copy_from`] which automatically
|
||||
/// fills out the [`EncodeMetadataApplicability`] as applicable to the chosen color space and
|
||||
/// other sample configuration.
|
||||
pub fn write_metadata_copy_from<R: Read + Seek>(
|
||||
&mut self,
|
||||
decoder: &mut Decoder<R>,
|
||||
options: EncodeMetadataOptions<'_>,
|
||||
kind: EncodeMetadataApplicability,
|
||||
) -> TiffResult<EncodeMetadataReport> {
|
||||
let mut report = EncodeMetadataReport {
|
||||
copied: 0,
|
||||
filtered: 0,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Ifd,
|
||||
ExifPrivate,
|
||||
GpsPrivate,
|
||||
}
|
||||
|
||||
let ptr = decoder.ifd_pointer().unwrap();
|
||||
let mut stack = vec![(ptr, Role::Ifd, usize::MAX)];
|
||||
let mut buf = ValueBuffer::empty(Type::UNDEFINED);
|
||||
|
||||
// Directories which we will later write in reverse-pre-order.
|
||||
let mut pre_order_dirs: Vec<(Directory, usize, Tag)> = vec![];
|
||||
|
||||
while let Some((ifd, role, parent)) = stack.pop() {
|
||||
let dir = decoder.read_directory(ifd)?;
|
||||
let mut created_dir = Directory::empty();
|
||||
|
||||
let filter = match role {
|
||||
Role::Ifd => TagFilter::filter_primary,
|
||||
Role::ExifPrivate => TagFilter::filter_exif_private,
|
||||
Role::GpsPrivate => TagFilter::filter_gps_private,
|
||||
};
|
||||
|
||||
let mut ifd_reader = decoder.read_directory_tags(&dir);
|
||||
|
||||
for (tag, entry) in dir.iter() {
|
||||
let level = filter(options.tag_filter, tag.to_u16());
|
||||
match options
|
||||
.tag_filter
|
||||
.should_keep_for_image_writer(level, &kind)
|
||||
{
|
||||
tag_filter::Choice::Ok => {}
|
||||
tag_filter::Choice::Descend(subifd_kind) => {
|
||||
// Only single pointers can be directories to descend to, avoid an overly
|
||||
// large read by pulling that value out of the file.
|
||||
if entry.count() != 1 {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
|
||||
// Should not happen (we're iterating the dict already), but be robust.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ifd) = buf.as_ifd_pointer() else {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let role = match subifd_kind {
|
||||
tag_filter::SubifdKind::ExifPrivate => Role::ExifPrivate,
|
||||
tag_filter::SubifdKind::Gps => Role::GpsPrivate,
|
||||
tag_filter::SubifdKind::Interoperability => {
|
||||
// Only applicable to compressed images.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let this_as_parent = pre_order_dirs.len();
|
||||
stack.push((ifd, role, this_as_parent));
|
||||
}
|
||||
tag_filter::Choice::Discard => continue,
|
||||
tag_filter::Choice::Filtered
|
||||
// Not supported yet.
|
||||
| tag_filter::Choice::Special(tag_filter::Special::JpegThumbnail) => {
|
||||
// Not to be copied.
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
tag_filter::Choice::Special(tag_filter::Special::CompressedExclusive) => {
|
||||
// We're not writing an App segment for a compressed file, are we?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ifd_reader.find_tag_buf(tag, &mut buf)?.is_none() {
|
||||
report.filtered = report.filtered.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-encode the value to the target file.
|
||||
buf.set_byte_order(ByteOrder::native());
|
||||
let entry = self.write_entry_buf(&buf)?;
|
||||
created_dir.extend([(tag, entry)]);
|
||||
report.copied = report.copied.saturating_add(1);
|
||||
}
|
||||
|
||||
let tag_in_parent = match role {
|
||||
// should be unreachable? For now. But thumbnails would use this so we best be a
|
||||
// little cautious here. Also thumbnails are weird, they do have image data but we
|
||||
// would not copy that data itself with these methods. Just eh.
|
||||
//
|
||||
// Anyways the tree root will use this tag as a dummy.
|
||||
Role::Ifd => Tag::SubIfd,
|
||||
Role::ExifPrivate => Tag::ExifDirectory,
|
||||
Role::GpsPrivate => Tag::GpsDirectory,
|
||||
};
|
||||
|
||||
pre_order_dirs.push((created_dir, parent, tag_in_parent));
|
||||
}
|
||||
|
||||
// Now post-order traverse the directory tree.
|
||||
while let Some((directory, parent, tag)) = pre_order_dirs.pop() {
|
||||
// The root is the items for the IFD itself.
|
||||
if pre_order_dirs.is_empty() {
|
||||
// We do not enter ourselves into a parent directory.
|
||||
let _ = (parent, tag);
|
||||
|
||||
self.extend_from(&directory);
|
||||
break;
|
||||
}
|
||||
|
||||
let offset = Self::write_directory_inner(self.writer, &directory)?;
|
||||
let offset = K::convert_offset(offset)?;
|
||||
let entry = self.write_entry(offset)?;
|
||||
|
||||
let (parent_dir, _, _) = &mut pre_order_dirs[parent];
|
||||
parent_dir.extend([(tag, entry)]);
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Define the parent directory.
|
||||
///
|
||||
/// Each directory has an offset based link to its successor, forming a linked list in the
|
||||
@@ -460,17 +605,21 @@ impl<'a, W: 'a + Write + Seek, K: TiffKind> DirectoryEncoder<'a, W, K> {
|
||||
}
|
||||
|
||||
fn write_directory(&mut self) -> TiffResult<u64> {
|
||||
Self::write_directory_inner(self.writer, &self.directory)
|
||||
}
|
||||
|
||||
fn write_directory_inner(writer: &mut TiffWriter<W>, directory: &Directory) -> TiffResult<u64> {
|
||||
// Start by turning all buffered unwritten values into entries.
|
||||
let offset = self.writer.offset();
|
||||
K::write_entry_count(self.writer, self.directory.len())?;
|
||||
let offset = writer.offset();
|
||||
K::write_entry_count(writer, directory.len())?;
|
||||
|
||||
let offset_bytes = mem::size_of::<K::OffsetType>();
|
||||
for (tag, entry) in self.directory.iter() {
|
||||
self.writer.write_u16(tag.to_u16())?;
|
||||
self.writer.write_u16(entry.field_type().to_u16())?;
|
||||
for (tag, entry) in directory.iter() {
|
||||
writer.write_u16(tag.to_u16())?;
|
||||
writer.write_u16(entry.field_type().to_u16())?;
|
||||
let count = K::convert_offset(entry.count())?;
|
||||
count.write(self.writer)?;
|
||||
self.writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||
count.write(writer)?;
|
||||
writer.write_bytes(&entry.offset()[..offset_bytes])?;
|
||||
}
|
||||
|
||||
Ok(offset)
|
||||
@@ -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.
|
||||
///
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
+58
@@ -407,6 +407,32 @@ 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();
|
||||
@@ -531,3 +557,35 @@ pub(crate) enum EndianBytes {
|
||||
Four,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_ifd_u32() {
|
||||
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd(0x42));
|
||||
|
||||
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
|
||||
value.set_byte_order(bo);
|
||||
|
||||
let ptr = value
|
||||
.as_ifd_pointer()
|
||||
.expect("Should be convertible to IfdPointer");
|
||||
|
||||
// Should be in host byte order again.
|
||||
assert_eq!(ptr.0, 0x42);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_ifd_long8() {
|
||||
let mut value = ValueBuffer::from_value(&crate::encoder::Ifd8(0x42));
|
||||
|
||||
for bo in [ByteOrder::BigEndian, ByteOrder::LittleEndian] {
|
||||
value.set_byte_order(bo);
|
||||
|
||||
let ptr = value
|
||||
.as_ifd_pointer()
|
||||
.expect("Should be convertible to IfdPointer");
|
||||
|
||||
// Should be in host byte order again.
|
||||
assert_eq!(ptr.0, 0x42);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use tiff::{
|
||||
decoder::Decoder,
|
||||
encoder::{colortype::Gray8, EncodeMetadataOptions, TiffEncoder},
|
||||
tags::Tag,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn copies_exif_data() {
|
||||
let src = std::path::PathBuf::from(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/images/predictor-3-gray-f32.tif"
|
||||
));
|
||||
|
||||
let src = std::fs::File::open(src).unwrap();
|
||||
let mut src = Decoder::new(src).unwrap();
|
||||
|
||||
let mut data = std::io::Cursor::new(vec![]);
|
||||
{
|
||||
let mut dst = TiffEncoder::new(&mut data).unwrap();
|
||||
let mut img = dst.new_image::<Gray8>(1, 1).unwrap();
|
||||
|
||||
let report = img
|
||||
.write_metadata_copy_from(&mut src, EncodeMetadataOptions::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(report.copied, 8);
|
||||
assert_eq!(report.filtered, 0, "There are no tags we do not recognize");
|
||||
img.write_data(&[0x80]).unwrap();
|
||||
}
|
||||
|
||||
data.set_position(0);
|
||||
|
||||
{
|
||||
let mut roundtrip = Decoder::new(data).unwrap();
|
||||
assert!(roundtrip.find_tag(Tag::DateTime).unwrap().is_some());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user