use alloc::sync::Arc;
use crate::Orientation;
use crate::info::{Cicp, ContentLightLevel, 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 orientation: Orientation,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(core::mem::size_of::<Metadata>() == 104);
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
}
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_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)
}
}
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,
orientation: info.orientation,
}
}
}
fn parse_exif_orientation(blob: &[u8]) -> Option<Orientation> {
if blob.len() < 8 {
return None;
}
let little_endian = match &blob[0..4] {
[b'I', b'I', 0x2a, 0x00] => true,
[b'M', b'M', 0x00, 0x2a] => false,
_ => return None,
};
let read_u16 = |offset: usize| -> Option<u16> {
let bytes = blob.get(offset..offset + 2)?;
Some(if little_endian {
u16::from_le_bytes([bytes[0], bytes[1]])
} else {
u16::from_be_bytes([bytes[0], bytes[1]])
})
};
let read_u32 = |offset: usize| -> Option<u32> {
let bytes = blob.get(offset..offset + 4)?;
Some(if little_endian {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
} else {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
})
};
let ifd0_offset = read_u32(4)? as usize;
let entry_count = read_u16(ifd0_offset)? as usize;
let entries_start = ifd0_offset.checked_add(2)?;
for i in 0..entry_count {
let entry_offset = entries_start.checked_add(i.checked_mul(12)?)?;
let tag = read_u16(entry_offset)?;
if tag == 0x0112 {
let value = read_u16(entry_offset + 8)?;
if value > 0 && value <= 8 {
return Orientation::from_exif(value as u8);
}
}
}
None
}
#[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);
}
#[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);
}
}