#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct URational {
pub numerator: u32,
pub denominator: u32,
}
impl URational {
pub fn new(numerator: u32, denominator: u32) -> Self {
Self {
numerator,
denominator,
}
}
pub fn to_f64(&self) -> f64 {
if self.denominator == 0 {
f64::NAN
} else {
self.numerator as f64 / self.denominator as f64
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SRational {
pub numerator: i32,
pub denominator: i32,
}
impl SRational {
pub fn new(numerator: i32, denominator: i32) -> Self {
Self {
numerator,
denominator,
}
}
pub fn to_f64(&self) -> f64 {
if self.denominator == 0 {
f64::NAN
} else {
self.numerator as f64 / self.denominator as f64
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CameraInfo {
pub make: String,
pub model: String,
pub unique_camera_model: Option<String>,
pub lens_make: Option<String>,
pub lens_model: Option<String>,
pub lens_info: Option<[f64; 4]>,
pub serial_number: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExifInfo {
pub iso: Option<u32>,
pub exposure_time: Option<URational>,
pub f_number: Option<URational>,
pub focal_length: Option<URational>,
pub focal_length_35mm: Option<u16>,
pub exposure_program: Option<u16>,
pub metering_mode: Option<u16>,
pub flash: Option<u16>,
pub exposure_compensation: Option<SRational>,
pub max_aperture: Option<URational>,
pub brightness_value: Option<SRational>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DateTimeInfo {
pub datetime_original: Option<String>,
pub create_date: Option<String>,
pub modify_date: Option<String>,
pub offset_time: Option<String>,
pub subsec_time: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GpsInfo {
pub latitude: Option<[URational; 3]>,
pub latitude_ref: Option<char>,
pub longitude: Option<[URational; 3]>,
pub longitude_ref: Option<char>,
pub altitude: Option<URational>,
pub altitude_ref: Option<u8>,
pub timestamp: Option<[URational; 3]>,
pub datestamp: Option<String>,
pub speed: Option<URational>,
pub img_direction: Option<URational>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DngColorInfo {
pub color_matrix_1: Option<[f64; 9]>,
pub color_matrix_2: Option<[f64; 9]>,
pub calibration_illuminant_1: Option<u16>,
pub calibration_illuminant_2: Option<u16>,
pub as_shot_neutral: Option<[f64; 3]>,
pub analog_balance: Option<[f64; 3]>,
pub white_balance: Option<String>,
pub color_temperature: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DngCalibrationInfo {
pub baseline_exposure: Option<f64>,
pub baseline_noise: Option<f64>,
pub baseline_sharpness: Option<f64>,
pub noise_profile: Option<Vec<f64>>,
pub noise_reduction_applied: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DngProfileInfo {
pub profile_name: Option<String>,
pub profile_tone_curve: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageInfo {
pub orientation: Option<u16>,
pub bit_depth: u8,
pub black_levels: Vec<u32>,
pub white_level: Option<u32>,
pub default_crop_origin: Option<(u32, u32)>,
pub default_crop_size: Option<(u32, u32)>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MetadataValue {
U64(u64),
I64(i64),
F64(f64),
URational(URational),
SRational(SRational),
Text(String),
Bytes(Vec<u8>),
Array(Vec<MetadataValue>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MetadataNamespace {
Exif,
Gps,
MakerNote,
Xmp,
Iptc,
Heic,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MetadataKey {
pub namespace: MetadataNamespace,
pub tag: String,
}
impl MetadataKey {
pub fn new(namespace: MetadataNamespace, tag: impl Into<String>) -> Self {
Self {
namespace,
tag: tag.into(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MetadataEntry {
pub key: MetadataKey,
pub value: MetadataValue,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageMetadata {
pub camera: CameraInfo,
pub exif: ExifInfo,
pub datetime: DateTimeInfo,
pub gps: GpsInfo,
pub dng_color: DngColorInfo,
pub dng_calibration: DngCalibrationInfo,
pub dng_profile: DngProfileInfo,
pub image: ImageInfo,
pub xmp: Option<Vec<u8>>,
pub icc_profile: Option<Vec<u8>>,
pub exif_raw: Option<Vec<u8>>,
pub makernote_raw: Option<Vec<u8>>,
pub iptc_raw: Option<Vec<u8>>,
pub extra: Vec<MetadataEntry>,
}
impl ImageMetadata {
pub fn get(&self, namespace: MetadataNamespace, tag: &str) -> Option<&MetadataValue> {
self.extra
.iter()
.find(|e| e.key.namespace == namespace && e.key.tag == tag)
.map(|e| &e.value)
}
pub fn insert(&mut self, key: MetadataKey, value: MetadataValue) {
if let Some(entry) = self.extra.iter_mut().find(|e| e.key == key) {
entry.value = value;
} else {
self.extra.push(MetadataEntry { key, value });
}
}
}
pub trait MetadataExtractor {
fn extract_metadata(&self) -> ImageMetadata;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_metadata_default() {
let meta = ImageMetadata::default();
assert!(meta.camera.make.is_empty());
assert!(meta.exif.iso.is_none());
assert!(meta.gps.latitude.is_none());
}
#[test]
fn test_rational_types() {
let ur = URational::new(1, 100);
assert_eq!(ur.numerator, 1);
assert_eq!(ur.denominator, 100);
let sr = SRational::new(-1, 3);
assert_eq!(sr.numerator, -1);
assert_eq!(sr.denominator, 3);
}
#[test]
fn test_metadata_value_variants() {
assert_eq!(MetadataValue::U64(42), MetadataValue::U64(42));
assert_ne!(MetadataValue::U64(1), MetadataValue::I64(1));
let nested = MetadataValue::Array(vec![
MetadataValue::Text("a".into()),
MetadataValue::Bytes(vec![1, 2, 3]),
MetadataValue::URational(URational::new(1, 2)),
]);
match nested {
MetadataValue::Array(items) => assert_eq!(items.len(), 3),
_ => panic!("expected Array"),
}
}
#[test]
fn test_extra_get_insert() {
let mut md = ImageMetadata::default();
assert!(md.get(MetadataNamespace::Exif, "0x9209").is_none());
md.insert(
MetadataKey::new(MetadataNamespace::Exif, "0x9209"),
MetadataValue::U64(9),
);
assert_eq!(md.extra.len(), 1);
assert_eq!(
md.get(MetadataNamespace::Exif, "0x9209"),
Some(&MetadataValue::U64(9))
);
md.insert(
MetadataKey::new(MetadataNamespace::Exif, "0x9209"),
MetadataValue::U64(16),
);
assert_eq!(md.extra.len(), 1);
assert_eq!(
md.get(MetadataNamespace::Exif, "0x9209"),
Some(&MetadataValue::U64(16))
);
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "0x9209"),
MetadataValue::Text("hi".into()),
);
assert_eq!(md.extra.len(), 2);
assert!(md.get(MetadataNamespace::Gps, "0x9209").is_none());
}
#[cfg(feature = "serde")]
#[test]
fn test_image_metadata_serde_roundtrip_with_extra() {
let mut md = ImageMetadata {
icc_profile: Some(vec![0xAA, 0xBB]),
exif_raw: Some(vec![1, 2, 3, 4]),
..Default::default()
};
md.insert(
MetadataKey::new(MetadataNamespace::Exif, "FlashEnergy"),
MetadataValue::Array(vec![
MetadataValue::URational(URational::new(3, 2)),
MetadataValue::F64(1.5),
]),
);
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "aux_count"),
MetadataValue::U64(2),
);
let json = serde_json::to_string(&md).expect("serialize");
let back: ImageMetadata = serde_json::from_str(&json).expect("deserialize");
assert_eq!(md, back, "ImageMetadata must survive a JSON round-trip");
}
}