diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 8f1cb20..f62c2b2 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -9,7 +9,7 @@ use std::{ }; use crate::{ - decoder::ifd::Entry, + decoder::{ifd::Entry, IfdDecoder}, error::{TiffResult, UsageError}, tags::{ ByteOrder, CompressionMethod, ExtraSamples, IfdPointer, PhotometricInterpretation, @@ -22,6 +22,7 @@ pub mod colortype; pub mod compression; mod tiff_value; mod writer; +mod tag_filter; use self::colortype::*; use self::compression::Compression as Comp; @@ -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,22 @@ 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. + pub fn write_metadata_from( + &mut self, + mut decoder: IfdDecoder<'_>, + options: EncodeMetadataOptions<'_>, + ) -> TiffResult { + let mut report = EncodeMetadataReport { + }; + + for tag in decoder.tag_iter() { + let (tag, entry) = tag?; + } + + Ok(report) + } + /// Define the parent directory. /// /// Each directory has an offset based link to its successor, forming a linked list in the @@ -582,6 +601,34 @@ 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, + /// 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 skip_invalid_tags: bool, +} + +#[non_exhaustive] +pub struct EncodeMetadataReport { +} + /// Type to encode images strip by strip. /// /// You should call `finish` on this when you are finished with it. diff --git a/src/encoder/tag_filter.rs b/src/encoder/tag_filter.rs new file mode 100644 index 0000000..a010ed7 --- /dev/null +++ b/src/encoder/tag_filter.rs @@ -0,0 +1,171 @@ +/// A filter of tags copied in +/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`]. +pub struct TagFilter { + unknown: bool, +} + +impl TagFilter { + /// Return a filter that allows all tags from metadata subdirectories. + pub fn allow_all_in_subdirectories() -> &'static TagFilter { + static INSTANCE: TagFilter = TagFilter { unknown: true }; + &INSTANCE + } + + /// Return a filter that has an explicit allow list of tags, for each of a known list of + /// metadata subdirectories. + pub fn allow_known() -> &'static TagFilter { + static INSTANCE: TagFilter = TagFilter { unknown: false }; + &INSTANCE + } + + /// 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, + 0x010e..=0x0110 => Level::Basic, + // strip offsets + 0x0111 => Level::SampleLayout, + 0x0112 => Level::Basic, + // strip byte counts, samples + 0x0115..=0x0117 => Level::SampleLayout, + 0x011a..=0x011b => Level::Basic, + // planar configuration + 0x011c => Level::Photometric(Photometric::Planar), + 0x0128 => Level::Basic, + // transfer function + 0x012d => Level::Photometric(Photometric::All), + 0x0131..=0x0132 => Level::Basic, + 0x013b => Level::Basic, + // 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), + 0x8298 => Level::Basic, + 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::Basic, + _ => Level::Unknown, + } + } + + pub(crate) fn filter_exif_private(&self, tag: u16) -> Level { + match tag { + 0x829a => Level::Basic, + 0x829d => Level::Basic, + 0x8822 => Level::Basic, + 0x8824 => Level::Basic, + 0x8827 => Level::Basic, + 0x8828 => Level::Basic, + 0x8830..=0x8835 => Level::Basic, + 0x9000 => Level::Basic, + 0x9003..=0x9004 => Level::Basic, + 0x9010..=0x9012 => Level::Basic, + // Not allowed for uncompressed data! + 0x9101..=0x9102 => Level::Special(Special::CompressedExclusive), + 0x9201..=0x9209 => Level::Basic, + 0x920a => Level::Basic, + 0x9214 => Level::Basic, + 0x927c => Level::Basic, + 0x9286 => Level::Basic, + 0x9290..=0x9292 => Level::Basic, + 0x9400..=0x9405 => Level::Basic, + 0xa000 => Level::Basic, + 0xa001 => Level::Photometric(Photometric::All), + 0xa002..=0xa003 => Level::Special(Special::CompressedExclusive), + 0xa004 => Level::Basic, + 0xa005 => Level::Ifd(SubifdKind::Interoperability), + 0xa20b..=0xa20c => Level::Basic, + 0xa20e..=0xa210 => Level::Basic, + 0xa214..=0xa215 => Level::Basic, + 0xa217 => Level::Basic, + 0xa300..=0xa302 => Level::Basic, + 0xa401..=0xa40c => Level::Basic, + 0xa420 => Level::Basic, + 0xa430..=0xa43c => Level::Basic, + 0xa460..=0xa462 => Level::Basic, + 0xa500 => Level::Photometric(Photometric::All), + _ => Level::Unknown, + } + } + + fn filter_gps_private(&self, tag: u16) -> Level { + match tag { + 0x0000..=0x001f => Level::Basic, + _ => Level::Unknown, + } + } + + fn filter_interoperability(&self, tag: u16) -> Level { + match tag { + 0x0001 => Level::Special(Special::CompressedExclusive), + _ => Level::Unknown, + } + } + + pub(crate) fn filter_secondary_exif(&self, tag: u16) -> Level { + // No difference for now. The only tabular difference I could spot is the treatment of + // sample positioning which is mandatory in the primary IFD of YCbCr but optional for all + // following IFDs.. + self.filter_primary_exif(tag) + } + + pub(crate) fn filter_secondary_gps(&self, tag: u16) -> Level { + self.filter_primary_gps(tag) + } +} + +/// 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. + SampleLayout, + /// Value is an IFD and must be copied by making a subdirectory. + Ifd(SubifdKind), + Photometric(Photometric), + Basic, + /// We recognize the tag but it can not be copied by itself through normal means. + 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 { + JpegThumbnail, + /// May only occur in compressed (JPEG APP segments) data. + CompressedExclusive, +}