Write tag filter based on tag kind/'Level'

This commit is contained in:
A. Molzer committed 2026-01-14 17:40:40 +01:00
1 parent 76721c6893
commit b3ced063ef
1 file changed
+169 -43
+169 -43
View File
@@ -1,23 +1,68 @@
use crate::tags::{PhotometricInterpretation, PlanarConfiguration};
/// A filter of tags copied in
/// [`DirectoryEncoder::write_metadata_from`][`super::DirectoryEncoder::write_metadata_from`].
pub struct TagFilter {
unknown: bool,
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 { unknown: true };
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 allow_known() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter { unknown: false };
pub fn only_known_tags() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::Known,
};
&INSTANCE
}
/// Return a filter that attempts to remove identifying tags. This is stricter than
/// [`Self::only_known_tags`] and excludes many tags except those describing the image data
/// layout.
///
/// **Note that it comes with *no warranty*.** although this will remove tags with prejudice.
pub fn attempt_at_privacy() -> &'static TagFilter {
static INSTANCE: TagFilter = TagFilter {
filter_level: FilterLevel::Privacy,
};
&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 {
@@ -25,20 +70,26 @@ impl TagFilter {
0x0100..=0x0103 => Level::SampleLayout,
// photometric interpretation
0x0106 => Level::SampleLayout,
0x010e..=0x0110 => Level::Basic,
// data tied to the camera and customized description, potentially PI.
0x010f..=0x0110 => Level::Value,
// strip offsets
0x0111 => Level::SampleLayout,
0x0112 => Level::Basic,
// orientation
0x0112 => Level::Value,
// strip byte counts, samples
0x0115..=0x0117 => Level::SampleLayout,
0x011a..=0x011b => Level::Basic,
// pixel dimensions (ratio). Should be generic enough.
0x011a..=0x011b => Level::Value,
// planar configuration
0x011c => Level::Photometric(Photometric::Planar),
0x0128 => Level::Basic,
// units for 0x011a,0x011b
0x0128 => Level::Value,
// transfer function
0x012d => Level::Photometric(Photometric::All),
0x0131..=0x0132 => Level::Basic,
0x013b => Level::Basic,
// 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
@@ -46,7 +97,8 @@ impl TagFilter {
0x0201..=0x0202 => Level::Special(Special::JpegThumbnail),
0x0211..=0x0213 => Level::Photometric(Photometric::YCbCr),
0x0214 => Level::Photometric(Photometric::All),
0x8298 => Level::Basic,
// Copyright holder (not same as artist).
0x8298 => Level::Value,
0x8769 => Level::Ifd(SubifdKind::ExifPrivate),
_ => Level::Unknown,
}
@@ -61,54 +113,57 @@ impl TagFilter {
pub(crate) fn filter_icc(&self, tag: u16) -> Level {
match tag {
0x8ff3 => Level::Basic,
0x8ff3 => Level::Value,
_ => 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,
// 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::Basic,
0x920a => Level::Basic,
0x9214 => Level::Basic,
0x927c => Level::Basic,
0x9286 => Level::Basic,
0x9290..=0x9292 => Level::Basic,
0x9400..=0x9405 => Level::Basic,
0xa000 => Level::Basic,
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::Basic,
0xa004 => Level::Value,
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,
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,
}
}
fn filter_gps_private(&self, tag: u16) -> Level {
pub(crate) fn filter_gps_private(&self, tag: u16) -> Level {
match tag {
0x0000..=0x001f => Level::Basic,
0x0000..=0x001f => Level::Value,
_ => Level::Unknown,
}
}
@@ -130,6 +185,61 @@ impl TagFilter {
pub(crate) fn filter_secondary_gps(&self, tag: u16) -> Level {
self.filter_primary_gps(tag)
}
pub(crate) fn should_keep_on_level(
&self,
level: Level,
target: &super::EncodeMetadataApplicability,
) -> Choice {
match level {
Level::Unknown if matches!(self.filter_level, FilterLevel::All) => Choice::Ok,
Level::SampleLayout => Choice::Ok,
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), _) => Choice::Ok,
(Photometric::Planar, None, _)
if matches!(self.filter_level, FilterLevel::All) =>
{
Choice::Ok
}
(Photometric::YCbCr, _, Some(PhotometricInterpretation::YCbCr)) => Choice::Ok,
(Photometric::YCbCr, _, None)
if matches!(self.filter_level, FilterLevel::All) =>
{
Choice::Ok
}
_ => Choice::Discard,
}
}
Level::Value if matches!(self.filter_level, FilterLevel::Privacy) => Choice::Discard,
Level::Value => Choice::Ok,
Level::Special(Special::JpegThumbnail)
if matches!(self.filter_level, FilterLevel::Privacy) =>
{
// Contains arbitrary unverified other data, so, no, we do not heed backward
// compatibility of a tag that should no longer be in active use if possible.
Choice::Discard
}
Level::Special(special) => Choice::Special(special),
_ => Choice::Discard,
}
}
}
pub(crate) enum Choice {
Ok,
Descend(SubifdKind),
Discard,
Special(Special),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FilterLevel {
All,
Known,
Privacy,
}
/// The level of support for a value.
@@ -138,12 +248,27 @@ 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),
Basic,
/// 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),
}
@@ -165,6 +290,7 @@ pub(crate) enum SubifdKind {
#[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,