use alloc::sync::Arc;
use crate::Orientation;
use crate::exif::{Exif, ExifPolicy, Retention};
use crate::info::{Cicp, ContentLightLevel, DiffuseWhite, MasteringDisplay};
use zenpixels::{ColorPrimaries, TransferFunction};
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Metadata {
pub icc_profile: Option<Arc<[u8]>>,
pub exif: Option<Arc<[u8]>>,
pub xmp: Option<Arc<[u8]>>,
pub cicp: Option<Cicp>,
pub content_light_level: Option<ContentLightLevel>,
pub mastering_display: Option<MasteringDisplay>,
pub diffuse_white: Option<DiffuseWhite>,
pub orientation: Orientation,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(core::mem::size_of::<Metadata>() == 112);
impl Metadata {
pub fn none() -> Self {
Self::default()
}
pub fn with_icc(mut self, icc: impl Into<Arc<[u8]>>) -> Self {
self.icc_profile = Some(icc.into());
self
}
pub fn with_exif(mut self, exif: impl Into<Arc<[u8]>>) -> Self {
let bytes: Arc<[u8]> = exif.into();
if self.orientation == Orientation::Identity
&& let Some(o) = parse_exif_orientation(&bytes)
{
self.orientation = o;
}
self.exif = Some(bytes);
self
}
#[must_use]
pub fn with_copyright(self, copyright: &str) -> Self {
self.set_exif_string(copyright, |e, t| e.set_copyright(t))
}
#[must_use]
pub fn with_artist(self, artist: &str) -> Self {
self.set_exif_string(artist, |e, t| e.set_artist(t))
}
fn set_exif_string(mut self, text: &str, set: impl FnOnce(&mut Exif<'_>, &str)) -> Self {
let bytes = {
let mut exif = self
.exif
.as_deref()
.and_then(Exif::parse)
.unwrap_or_default();
set(&mut exif, text);
exif.to_bytes()
};
self.exif = Some(Arc::from(bytes));
self
}
pub fn with_xmp(mut self, xmp: impl Into<Arc<[u8]>>) -> Self {
self.xmp = Some(xmp.into());
self
}
pub fn with_cicp(mut self, cicp: Cicp) -> Self {
self.cicp = Some(cicp);
self
}
pub fn with_content_light_level(mut self, clli: ContentLightLevel) -> Self {
self.content_light_level = Some(clli);
self
}
pub fn with_diffuse_white(mut self, white: DiffuseWhite) -> Self {
self.diffuse_white = Some(white);
self
}
pub fn clear_diffuse_white(mut self) -> Self {
self.diffuse_white = None;
self
}
pub fn with_mastering_display(mut self, mdcv: MasteringDisplay) -> Self {
self.mastering_display = Some(mdcv);
self
}
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = orientation;
self
}
pub fn is_empty(&self) -> bool {
self.icc_profile.is_none()
&& self.exif.is_none()
&& self.xmp.is_none()
&& self.cicp.is_none()
&& self.content_light_level.is_none()
&& self.mastering_display.is_none()
&& self.orientation == Orientation::Identity
}
pub fn transfer_function(&self) -> TransferFunction {
self.cicp
.and_then(|c| TransferFunction::from_cicp(c.transfer_characteristics))
.unwrap_or(TransferFunction::Unknown)
}
pub fn color_primaries(&self) -> ColorPrimaries {
self.cicp
.map(|c| c.color_primaries_enum())
.unwrap_or(ColorPrimaries::Bt709)
}
#[must_use]
pub fn filtered(&self, policy: &MetadataPolicy) -> Metadata {
let f = policy.fields();
let mut out = Metadata::none();
out.icc_profile = match f.icc {
IccRetention::Drop => None,
IccRetention::Keep
| IccRetention::DropIfCicpRepresentable
| IccRetention::DropIfCicpSafeSoleCarrier => self.icc_profile.clone(),
IccRetention::KeepNonSrgb => self
.icc_profile
.as_ref()
.filter(|icc| !zenpixels::icc::is_common_srgb(icc))
.cloned(),
};
out.orientation = if f.exif.orientation.keeps() {
self.orientation
} else {
Orientation::Identity
};
if f.cicp.keeps() {
out.cicp = self.cicp;
}
if f.hdr.keeps() {
out.content_light_level = self.content_light_level;
out.mastering_display = self.mastering_display;
}
if f.xmp.keeps() {
out.xmp = self.xmp.clone();
}
out.exif = self.exif.as_ref().and_then(|src| {
match crate::exif::retain_reconciled(src, &f.exif, Some(out.orientation))? {
alloc::borrow::Cow::Borrowed(_) => Some(src.clone()),
alloc::borrow::Cow::Owned(v) => Some(Arc::from(v)),
}
});
out
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IccRetention {
Drop,
KeepNonSrgb,
Keep,
DropIfCicpRepresentable,
DropIfCicpSafeSoleCarrier,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct MetadataFields {
pub icc: IccRetention,
pub exif: ExifPolicy,
pub xmp: Retention,
pub cicp: Retention,
pub hdr: Retention,
}
impl MetadataFields {
pub const KEEP_ALL: Self = Self {
icc: IccRetention::Keep,
exif: ExifPolicy::KEEP_ALL,
xmp: Retention::Keep,
cicp: Retention::Keep,
hdr: Retention::Keep,
};
pub const DISCARD_ALL: Self = Self {
icc: IccRetention::Drop,
exif: ExifPolicy::DISCARD_ALL,
xmp: Retention::Discard,
cicp: Retention::Discard,
hdr: Retention::Discard,
};
#[must_use]
pub const fn with_icc(mut self, r: IccRetention) -> Self {
self.icc = r;
self
}
#[must_use]
pub const fn with_exif(mut self, p: ExifPolicy) -> Self {
self.exif = p;
self
}
#[must_use]
pub const fn with_xmp(mut self, r: Retention) -> Self {
self.xmp = r;
self
}
#[must_use]
pub const fn with_cicp(mut self, r: Retention) -> Self {
self.cicp = r;
self
}
#[must_use]
pub const fn with_hdr(mut self, r: Retention) -> Self {
self.hdr = r;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetadataPolicy {
PreserveExact,
Preserve,
Web,
ColorAndRotation,
Custom(MetadataFields),
}
impl MetadataPolicy {
#[must_use]
pub fn fields(&self) -> MetadataFields {
match self {
Self::PreserveExact => MetadataFields::KEEP_ALL,
Self::Preserve => MetadataFields {
icc: IccRetention::KeepNonSrgb,
..MetadataFields::KEEP_ALL
},
Self::Web => MetadataFields {
icc: IccRetention::KeepNonSrgb,
exif: ExifPolicy::ATTRIBUTED_ORIENTATION,
xmp: Retention::Discard,
cicp: Retention::Keep,
hdr: Retention::Keep,
},
Self::ColorAndRotation => MetadataFields {
icc: IccRetention::KeepNonSrgb,
exif: ExifPolicy::ORIENTATION_ONLY,
xmp: Retention::Discard,
cicp: Retention::Keep,
hdr: Retention::Keep,
},
Self::Custom(f) => *f,
}
}
}
impl From<&crate::ImageInfo> for Metadata {
fn from(info: &crate::ImageInfo) -> Self {
Self {
icc_profile: info.source_color.icc_profile.clone(),
exif: info.embedded_metadata.exif.clone(),
xmp: info.embedded_metadata.xmp.clone(),
cicp: info.source_color.cicp,
content_light_level: info.source_color.content_light_level,
mastering_display: info.source_color.mastering_display,
diffuse_white: info.source_color.diffuse_white,
orientation: info.orientation,
}
}
}
fn parse_exif_orientation(blob: &[u8]) -> Option<Orientation> {
crate::helpers::parse_exif_orientation(blob)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ImageFormat;
#[test]
fn metadata_roundtrip() {
let info = crate::ImageInfo::new(100, 200, ImageFormat::Jpeg)
.with_icc_profile(alloc::vec![1, 2, 3])
.with_exif(alloc::vec![4, 5])
.with_cicp(Cicp::SRGB)
.with_content_light_level(ContentLightLevel {
max_content_light_level: 1000,
max_frame_average_light_level: 400,
});
let meta = info.metadata();
assert_eq!(meta.icc_profile.as_deref(), Some([1, 2, 3].as_slice()));
assert_eq!(meta.exif.as_deref(), Some([4, 5].as_slice()));
assert!(meta.xmp.is_none());
assert_eq!(meta.cicp, Some(Cicp::SRGB));
assert_eq!(
meta.content_light_level.unwrap().max_content_light_level,
1000
);
assert!(meta.mastering_display.is_none());
assert!(!meta.is_empty());
}
#[test]
fn metadata_empty() {
let meta = Metadata::none();
assert!(meta.is_empty());
}
#[test]
fn metadata_with_cicp_not_empty() {
let meta = Metadata::none().with_cicp(Cicp::SRGB);
assert!(!meta.is_empty());
}
#[test]
fn metadata_with_hdr_not_empty() {
let meta = Metadata::none().with_content_light_level(ContentLightLevel {
max_content_light_level: 1000,
max_frame_average_light_level: 400,
});
assert!(!meta.is_empty());
}
#[test]
fn metadata_orientation_roundtrip() {
let info = crate::ImageInfo::new(100, 200, ImageFormat::Jpeg)
.with_orientation(Orientation::Rotate90);
let meta = info.metadata();
assert_eq!(meta.orientation, Orientation::Rotate90);
}
#[test]
fn metadata_orientation_default_is_normal() {
let meta = Metadata::none();
assert_eq!(meta.orientation, Orientation::Identity);
}
#[test]
fn metadata_with_orientation_builder() {
let meta = Metadata::none().with_orientation(Orientation::Rotate270);
assert_eq!(meta.orientation, Orientation::Rotate270);
}
#[test]
fn metadata_orientation_not_empty() {
let meta = Metadata::none().with_orientation(Orientation::Rotate90);
assert!(!meta.is_empty());
}
#[test]
fn metadata_identity_orientation_is_empty() {
let meta = Metadata::none().with_orientation(Orientation::Identity);
assert!(meta.is_empty());
}
#[test]
fn metadata_transfer_function() {
let meta = Metadata::none().with_cicp(Cicp::SRGB);
assert_eq!(meta.transfer_function(), TransferFunction::Srgb);
let meta = Metadata::none();
assert_eq!(meta.transfer_function(), TransferFunction::Unknown);
}
#[test]
fn metadata_builder() {
let meta = Metadata::none()
.with_icc(alloc::vec![1, 2, 3])
.with_exif(alloc::vec![4, 5])
.with_cicp(Cicp::SRGB)
.with_orientation(Orientation::Rotate90);
assert!(!meta.is_empty());
assert_eq!(meta.icc_profile.as_deref(), Some([1, 2, 3].as_slice()));
assert_eq!(meta.exif.as_deref(), Some([4, 5].as_slice()));
assert!(meta.xmp.is_none());
assert_eq!(meta.cicp, Some(Cicp::SRGB));
assert_eq!(meta.orientation, Orientation::Rotate90);
}
#[test]
fn metadata_from_image_info() {
let info = crate::ImageInfo::new(100, 200, ImageFormat::Jpeg)
.with_icc_profile(alloc::vec![10, 20, 30])
.with_exif(alloc::vec![4, 5])
.with_cicp(Cicp::SRGB)
.with_orientation(Orientation::Rotate270);
let meta = Metadata::from(&info);
assert_eq!(meta.icc_profile.as_deref(), Some([10, 20, 30].as_slice()));
assert_eq!(meta.exif.as_deref(), Some([4, 5].as_slice()));
assert_eq!(meta.cicp, Some(Cicp::SRGB));
assert_eq!(meta.orientation, Orientation::Rotate270);
}
fn build_minimal_exif_with_orientation(value: u16, big_endian: bool) -> alloc::vec::Vec<u8> {
let mut v = alloc::vec::Vec::new();
if big_endian {
v.extend_from_slice(b"MM\x00\x2a");
v.extend_from_slice(&8u32.to_be_bytes());
v.extend_from_slice(&1u16.to_be_bytes());
v.extend_from_slice(&0x0112u16.to_be_bytes());
v.extend_from_slice(&3u16.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&value.to_be_bytes());
v.extend_from_slice(&[0u8, 0]);
v.extend_from_slice(&0u32.to_be_bytes());
} else {
v.extend_from_slice(b"II\x2a\x00");
v.extend_from_slice(&8u32.to_le_bytes());
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&0x0112u16.to_le_bytes());
v.extend_from_slice(&3u16.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
v.extend_from_slice(&(value as u32).to_le_bytes());
v.extend_from_slice(&0u32.to_le_bytes());
}
v
}
#[test]
fn parse_exif_orientation_le_returns_correct_variant() {
let blob = build_minimal_exif_with_orientation(6, false);
assert_eq!(parse_exif_orientation(&blob), Some(Orientation::Rotate90));
}
#[test]
fn parse_exif_orientation_be_returns_correct_variant() {
let blob = build_minimal_exif_with_orientation(6, true);
assert_eq!(parse_exif_orientation(&blob), Some(Orientation::Rotate90));
}
#[test]
fn parse_exif_orientation_garbage_returns_none() {
assert_eq!(parse_exif_orientation(b"garbage"), None);
assert_eq!(parse_exif_orientation(&[]), None);
assert_eq!(parse_exif_orientation(&[0u8; 7]), None);
}
#[test]
fn with_exif_auto_parses_orientation_from_blob() {
let blob = build_minimal_exif_with_orientation(8, false);
let meta = Metadata::none().with_exif(blob);
assert_eq!(meta.orientation, Orientation::Rotate270);
}
fn build_exif_with_long_orientation(value: u32, big_endian: bool) -> alloc::vec::Vec<u8> {
let mut v = alloc::vec::Vec::new();
if big_endian {
v.extend_from_slice(b"MM\x00\x2a");
v.extend_from_slice(&8u32.to_be_bytes());
v.extend_from_slice(&1u16.to_be_bytes());
v.extend_from_slice(&0x0112u16.to_be_bytes());
v.extend_from_slice(&4u16.to_be_bytes()); v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&value.to_be_bytes());
} else {
v.extend_from_slice(b"II\x2a\x00");
v.extend_from_slice(&8u32.to_le_bytes());
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&0x0112u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes()); v.extend_from_slice(&1u32.to_le_bytes());
v.extend_from_slice(&value.to_le_bytes());
}
v
}
#[test]
fn parse_exif_orientation_accepts_long_type_be() {
let blob = build_exif_with_long_orientation(6, true);
assert_eq!(parse_exif_orientation(&blob), Some(Orientation::Rotate90));
}
#[test]
fn parse_exif_orientation_accepts_long_type_le() {
let blob = build_exif_with_long_orientation(8, false);
assert_eq!(parse_exif_orientation(&blob), Some(Orientation::Rotate270));
}
#[test]
fn with_exif_does_not_override_explicit_orientation() {
let blob = build_minimal_exif_with_orientation(6, false);
let meta = Metadata::none()
.with_orientation(Orientation::FlipH)
.with_exif(blob);
assert_eq!(meta.orientation, Orientation::FlipH);
}
use crate::exif::Exif;
fn src_exif(orientation: u16, copyright: &str, prefix: bool) -> alloc::vec::Vec<u8> {
use alloc::vec::Vec;
let mut cw = copyright.as_bytes().to_vec();
cw.push(0); let n: u16 = 3;
let ifd_size = 2 + 12 * n as usize + 4;
let ext_off = 8 + ifd_size;
let mut t = Vec::new();
t.extend_from_slice(b"II");
t.extend_from_slice(&42u16.to_le_bytes());
t.extend_from_slice(&8u32.to_le_bytes());
t.extend_from_slice(&n.to_le_bytes());
t.extend_from_slice(&0x010Fu16.to_le_bytes());
t.extend_from_slice(&2u16.to_le_bytes());
t.extend_from_slice(&4u32.to_le_bytes());
t.extend_from_slice(b"Cam\0");
t.extend_from_slice(&0x0112u16.to_le_bytes());
t.extend_from_slice(&3u16.to_le_bytes());
t.extend_from_slice(&1u32.to_le_bytes());
t.extend_from_slice(&u32::from(orientation).to_le_bytes());
t.extend_from_slice(&0x8298u16.to_le_bytes());
t.extend_from_slice(&2u16.to_le_bytes());
t.extend_from_slice(&(cw.len() as u32).to_le_bytes());
t.extend_from_slice(&(ext_off as u32).to_le_bytes());
t.extend_from_slice(&0u32.to_le_bytes()); t.extend_from_slice(&cw);
if prefix {
let mut out = Vec::with_capacity(6 + t.len());
out.extend_from_slice(b"Exif\0\0");
out.extend_from_slice(&t);
out
} else {
t
}
}
fn has_tag(blob: &[u8], tag: u16) -> bool {
blob.windows(2).any(|w| w == tag.to_le_bytes())
}
#[test]
fn policy_fields_resolution() {
assert_eq!(
MetadataPolicy::PreserveExact.fields(),
MetadataFields::KEEP_ALL
);
assert_eq!(
MetadataPolicy::PreserveExact.fields().icc,
IccRetention::Keep
);
assert_eq!(
MetadataPolicy::Preserve.fields().icc,
IccRetention::KeepNonSrgb
);
assert_eq!(MetadataPolicy::Preserve.fields().exif, ExifPolicy::KEEP_ALL);
let web = MetadataPolicy::Web.fields();
assert_eq!(web.icc, IccRetention::KeepNonSrgb);
assert_eq!(web.exif, ExifPolicy::ATTRIBUTED_ORIENTATION);
assert_eq!(web.xmp, Retention::Discard);
assert_eq!(web.cicp, Retention::Keep);
assert_eq!(web.hdr, Retention::Keep);
let car = MetadataPolicy::ColorAndRotation.fields();
assert_eq!(car.exif, ExifPolicy::ORIENTATION_ONLY);
assert_eq!(car.cicp, Retention::Keep);
let custom = MetadataFields {
xmp: Retention::Keep,
..MetadataFields::DISCARD_ALL
};
assert_eq!(MetadataPolicy::Custom(custom).fields(), custom);
}
#[test]
fn icc_three_way_retention() {
let icc = alloc::vec![0xABu8; 256]; let meta = Metadata::none().with_icc(icc.clone());
assert_eq!(
meta.filtered(&MetadataPolicy::Web).icc_profile.as_deref(),
Some(icc.as_slice())
);
assert_eq!(
meta.filtered(&MetadataPolicy::PreserveExact)
.icc_profile
.as_deref(),
Some(icc.as_slice())
);
let drop = MetadataFields {
icc: IccRetention::Drop,
..MetadataFields::KEEP_ALL
};
assert!(
meta.filtered(&MetadataPolicy::Custom(drop))
.icc_profile
.is_none()
);
}
#[test]
fn web_keeps_orientation_rights_drops_camera_and_xmp() {
let src = src_exif(6, "(c) 2026 Lilith", false);
let meta = Metadata::none()
.with_exif(src.clone())
.with_xmp(alloc::vec![1, 2, 3])
.with_cicp(Cicp::SRGB)
.with_content_light_level(ContentLightLevel {
max_content_light_level: 1000,
max_frame_average_light_level: 400,
});
assert_eq!(meta.orientation, Orientation::Rotate90);
let out = meta.filtered(&MetadataPolicy::Web);
let e = out.exif.as_deref().expect("rewritten EXIF");
let ex = Exif::parse(e).expect("parses");
assert_eq!(ex.orientation(), Some(Orientation::Rotate90));
assert_eq!(ex.copyright().unwrap(), "(c) 2026 Lilith");
assert!(!has_tag(e, 0x010F));
assert!(e.len() < src.len());
assert_eq!(out.orientation, Orientation::Rotate90);
assert!(out.xmp.is_none());
assert_eq!(out.cicp, Some(Cicp::SRGB));
assert!(out.content_light_level.is_some());
}
#[test]
fn preserve_exact_passes_exif_through_byte_identical() {
let src = src_exif(6, "(c) Owner", false);
let meta = Metadata::none()
.with_exif(src.clone())
.with_xmp(alloc::vec![9, 9])
.with_icc(alloc::vec![0xABu8; 200]);
let out = meta.filtered(&MetadataPolicy::PreserveExact);
assert_eq!(out.exif.as_deref(), Some(src.as_slice()));
assert!(has_tag(out.exif.as_deref().unwrap(), 0x010F)); assert_eq!(out.xmp.as_deref(), Some([9, 9].as_slice()));
assert!(out.icc_profile.is_some());
}
#[test]
fn color_and_rotation_keeps_orientation_drops_rights() {
let src = src_exif(8, "(c) Owner", false);
let meta = Metadata::none().with_exif(src).with_cicp(Cicp::SRGB);
let out = meta.filtered(&MetadataPolicy::ColorAndRotation);
let e = out.exif.as_deref().expect("EXIF");
let ex = Exif::parse(e).expect("parses");
assert_eq!(ex.orientation(), Some(Orientation::Rotate270));
assert!(ex.copyright().is_none()); assert!(!has_tag(e, 0x010F)); assert_eq!(out.cicp, Some(Cicp::SRGB));
}
#[test]
fn with_copyright_creates_blob_from_nothing() {
let meta = Metadata::none().with_copyright("(c) 2026 Lilith");
let e = meta.exif.as_deref().expect("EXIF created");
assert_eq!(
Exif::parse(e).unwrap().copyright().unwrap(),
"(c) 2026 Lilith"
);
}
#[test]
fn with_copyright_merges_into_existing() {
let src = src_exif(6, "old", false);
let meta = Metadata::none()
.with_exif(src)
.with_copyright("(c) New Owner");
let e = meta.exif.as_deref().expect("EXIF");
let x = Exif::parse(e).unwrap();
assert_eq!(x.copyright().unwrap(), "(c) New Owner"); assert_eq!(x.orientation(), Some(Orientation::Rotate90)); assert!(has_tag(e, 0x010F), "Make preserved on merge");
assert_eq!(meta.orientation, Orientation::Rotate90); }
#[test]
fn with_artist_creates_blob() {
let meta = Metadata::none().with_artist("Lilith");
let e = meta.exif.as_deref().expect("EXIF");
assert_eq!(Exif::parse(e).unwrap().artist().unwrap(), "Lilith");
}
#[test]
fn filtered_reconciles_baked_orientation_tag() {
let blob = src_exif(6, "(c) Owner", false);
let meta = Metadata::none()
.with_exif(blob) .with_orientation(Orientation::Identity); assert_eq!(meta.orientation, Orientation::Identity);
assert_eq!(
parse_exif_orientation(meta.exif.as_deref().unwrap()),
Some(Orientation::Rotate90)
);
let out = meta.filtered(&MetadataPolicy::PreserveExact);
assert_eq!(out.orientation, Orientation::Identity);
assert_eq!(
parse_exif_orientation(out.exif.as_deref().unwrap()),
Some(Orientation::Identity),
"baked-upright blob must be rewritten to Identity, not left at Rotate90"
);
}
#[test]
fn custom_drop_only_camera_keeps_rest() {
let src = src_exif(6, "(c) Owner", false);
let fields = MetadataFields {
exif: ExifPolicy {
camera: Retention::Discard,
..ExifPolicy::KEEP_ALL
},
..MetadataFields::KEEP_ALL
};
let out = Metadata::none()
.with_exif(src)
.filtered(&MetadataPolicy::Custom(fields));
let e = out.exif.as_deref().expect("EXIF");
let ex = Exif::parse(e).expect("parses");
assert_eq!(ex.orientation(), Some(Orientation::Rotate90));
assert_eq!(ex.copyright().unwrap(), "(c) Owner");
assert!(!has_tag(e, 0x010F)); }
#[test]
fn dropping_orientation_resets_field_to_identity() {
let meta = Metadata::none().with_orientation(Orientation::Rotate90);
let fields = MetadataFields {
exif: ExifPolicy {
orientation: Retention::Discard,
..ExifPolicy::KEEP_ALL
},
..MetadataFields::KEEP_ALL
};
let out = meta.filtered(&MetadataPolicy::Custom(fields));
assert_eq!(out.orientation, Orientation::Identity);
}
#[test]
fn exif_prefix_preserved_through_rewrite() {
let src = src_exif(6, "(c) Owner", true); let out = Metadata::none()
.with_exif(src)
.filtered(&MetadataPolicy::Web);
let e = out.exif.as_deref().expect("EXIF");
assert_eq!(&e[..6], b"Exif\0\0");
let ex = Exif::parse(e).expect("parses");
assert_eq!(ex.orientation(), Some(Orientation::Rotate90));
assert_eq!(ex.copyright().unwrap(), "(c) Owner");
}
#[test]
fn filtered_empty_metadata_is_empty() {
for p in [
MetadataPolicy::PreserveExact,
MetadataPolicy::Preserve,
MetadataPolicy::Web,
MetadataPolicy::ColorAndRotation,
] {
assert!(Metadata::none().filtered(&p).is_empty());
}
}
}