1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct URational {
15 pub numerator: u32,
17 pub denominator: u32,
19}
20
21impl URational {
22 pub fn new(numerator: u32, denominator: u32) -> Self {
24 Self {
25 numerator,
26 denominator,
27 }
28 }
29 pub fn to_f64(&self) -> f64 {
31 if self.denominator == 0 {
32 f64::NAN
33 } else {
34 self.numerator as f64 / self.denominator as f64
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct SRational {
45 pub numerator: i32,
47 pub denominator: i32,
49}
50
51impl SRational {
52 pub fn new(numerator: i32, denominator: i32) -> Self {
54 Self {
55 numerator,
56 denominator,
57 }
58 }
59 pub fn to_f64(&self) -> f64 {
61 if self.denominator == 0 {
62 f64::NAN
63 } else {
64 self.numerator as f64 / self.denominator as f64
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default, PartialEq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct CameraInfo {
73 pub make: String,
75 pub model: String,
77 pub unique_camera_model: Option<String>,
79 pub lens_make: Option<String>,
81 pub lens_model: Option<String>,
83 pub lens_info: Option<[f64; 4]>,
85 pub serial_number: Option<String>,
87}
88
89#[derive(Debug, Clone, Default, PartialEq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct ExifInfo {
93 pub iso: Option<u32>,
95 pub exposure_time: Option<URational>,
97 pub f_number: Option<URational>,
99 pub focal_length: Option<URational>,
101 pub focal_length_35mm: Option<u16>,
103 pub exposure_program: Option<u16>,
105 pub metering_mode: Option<u16>,
107 pub flash: Option<u16>,
109 pub exposure_compensation: Option<SRational>,
111 pub max_aperture: Option<URational>,
113 pub brightness_value: Option<SRational>,
115}
116
117#[derive(Debug, Clone, Default, PartialEq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub struct DateTimeInfo {
121 pub datetime_original: Option<String>,
123 pub create_date: Option<String>,
125 pub modify_date: Option<String>,
127 pub offset_time: Option<String>,
129 pub subsec_time: Option<String>,
131}
132
133#[derive(Debug, Clone, Default, PartialEq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136pub struct GpsInfo {
137 pub latitude: Option<[URational; 3]>,
139 pub latitude_ref: Option<char>,
141 pub longitude: Option<[URational; 3]>,
143 pub longitude_ref: Option<char>,
145 pub altitude: Option<URational>,
147 pub altitude_ref: Option<u8>,
149 pub timestamp: Option<[URational; 3]>,
151 pub datestamp: Option<String>,
153 pub speed: Option<URational>,
155 pub img_direction: Option<URational>,
157}
158
159#[derive(Debug, Clone, Default, PartialEq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162pub struct DngColorInfo {
163 pub color_matrix_1: Option<[f64; 9]>,
165 pub color_matrix_2: Option<[f64; 9]>,
167 pub calibration_illuminant_1: Option<u16>,
169 pub calibration_illuminant_2: Option<u16>,
171 pub as_shot_neutral: Option<[f64; 3]>,
173 pub analog_balance: Option<[f64; 3]>,
175 pub white_balance: Option<String>,
177 pub color_temperature: Option<u32>,
179}
180
181#[derive(Debug, Clone, Default, PartialEq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184pub struct DngCalibrationInfo {
185 pub baseline_exposure: Option<f64>,
187 pub baseline_noise: Option<f64>,
189 pub baseline_sharpness: Option<f64>,
191 pub noise_profile: Option<Vec<f64>>,
193 pub noise_reduction_applied: Option<f64>,
195}
196
197#[derive(Debug, Clone, Default, PartialEq)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct DngProfileInfo {
201 pub profile_name: Option<String>,
203 pub profile_tone_curve: Option<Vec<f32>>,
205}
206
207#[derive(Debug, Clone, Default, PartialEq)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub struct ImageInfo {
211 pub orientation: Option<u16>,
213 pub bit_depth: u8,
215 pub black_levels: Vec<u32>,
217 pub white_level: Option<u32>,
219 pub default_crop_origin: Option<(u32, u32)>,
221 pub default_crop_size: Option<(u32, u32)>,
223}
224
225#[derive(Debug, Clone, PartialEq)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233pub enum MetadataValue {
234 U64(u64),
236 I64(i64),
238 F64(f64),
240 URational(URational),
242 SRational(SRational),
244 Text(String),
246 Bytes(Vec<u8>),
248 Array(Vec<MetadataValue>),
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255pub enum MetadataNamespace {
256 Exif,
258 Gps,
260 MakerNote,
262 Xmp,
264 Iptc,
266 Heic,
268 Other,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Hash)]
274#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
275pub struct MetadataKey {
276 pub namespace: MetadataNamespace,
278 pub tag: String,
281}
282
283impl MetadataKey {
284 pub fn new(namespace: MetadataNamespace, tag: impl Into<String>) -> Self {
286 Self {
287 namespace,
288 tag: tag.into(),
289 }
290 }
291}
292
293#[derive(Debug, Clone, PartialEq)]
295#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
296pub struct MetadataEntry {
297 pub key: MetadataKey,
299 pub value: MetadataValue,
301}
302
303#[derive(Debug, Clone, Default, PartialEq)]
311#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
312pub struct ImageMetadata {
313 pub camera: CameraInfo,
315 pub exif: ExifInfo,
317 pub datetime: DateTimeInfo,
319 pub gps: GpsInfo,
321 pub dng_color: DngColorInfo,
323 pub dng_calibration: DngCalibrationInfo,
325 pub dng_profile: DngProfileInfo,
327 pub image: ImageInfo,
329 pub xmp: Option<Vec<u8>>,
331 pub icc_profile: Option<Vec<u8>>,
333 pub exif_raw: Option<Vec<u8>>,
335 pub makernote_raw: Option<Vec<u8>>,
337 pub iptc_raw: Option<Vec<u8>>,
339 pub extra: Vec<MetadataEntry>,
349}
350
351impl ImageMetadata {
352 pub fn get(&self, namespace: MetadataNamespace, tag: &str) -> Option<&MetadataValue> {
356 self.extra
357 .iter()
358 .find(|e| e.key.namespace == namespace && e.key.tag == tag)
359 .map(|e| &e.value)
360 }
361
362 pub fn insert(&mut self, key: MetadataKey, value: MetadataValue) {
367 if let Some(entry) = self.extra.iter_mut().find(|e| e.key == key) {
368 entry.value = value;
369 } else {
370 self.extra.push(MetadataEntry { key, value });
371 }
372 }
373}
374
375pub trait MetadataExtractor {
380 fn extract_metadata(&self) -> ImageMetadata;
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_image_metadata_default() {
390 let meta = ImageMetadata::default();
391 assert!(meta.camera.make.is_empty());
392 assert!(meta.exif.iso.is_none());
393 assert!(meta.gps.latitude.is_none());
394 }
395
396 #[test]
397 fn test_rational_types() {
398 let ur = URational::new(1, 100);
399 assert_eq!(ur.numerator, 1);
400 assert_eq!(ur.denominator, 100);
401
402 let sr = SRational::new(-1, 3);
403 assert_eq!(sr.numerator, -1);
404 assert_eq!(sr.denominator, 3);
405 }
406
407 #[test]
408 fn test_metadata_value_variants() {
409 assert_eq!(MetadataValue::U64(42), MetadataValue::U64(42));
411 assert_ne!(MetadataValue::U64(1), MetadataValue::I64(1));
412 let nested = MetadataValue::Array(vec![
413 MetadataValue::Text("a".into()),
414 MetadataValue::Bytes(vec![1, 2, 3]),
415 MetadataValue::URational(URational::new(1, 2)),
416 ]);
417 match nested {
418 MetadataValue::Array(items) => assert_eq!(items.len(), 3),
419 _ => panic!("expected Array"),
420 }
421 }
422
423 #[test]
424 fn test_extra_get_insert() {
425 let mut md = ImageMetadata::default();
426 assert!(md.get(MetadataNamespace::Exif, "0x9209").is_none());
427
428 md.insert(
429 MetadataKey::new(MetadataNamespace::Exif, "0x9209"),
430 MetadataValue::U64(9),
431 );
432 assert_eq!(md.extra.len(), 1);
433 assert_eq!(
434 md.get(MetadataNamespace::Exif, "0x9209"),
435 Some(&MetadataValue::U64(9))
436 );
437
438 md.insert(
440 MetadataKey::new(MetadataNamespace::Exif, "0x9209"),
441 MetadataValue::U64(16),
442 );
443 assert_eq!(md.extra.len(), 1);
444 assert_eq!(
445 md.get(MetadataNamespace::Exif, "0x9209"),
446 Some(&MetadataValue::U64(16))
447 );
448
449 md.insert(
451 MetadataKey::new(MetadataNamespace::Heic, "0x9209"),
452 MetadataValue::Text("hi".into()),
453 );
454 assert_eq!(md.extra.len(), 2);
455 assert!(md.get(MetadataNamespace::Gps, "0x9209").is_none());
456 }
457
458 #[cfg(feature = "serde")]
459 #[test]
460 fn test_image_metadata_serde_roundtrip_with_extra() {
461 let mut md = ImageMetadata {
462 icc_profile: Some(vec![0xAA, 0xBB]),
463 exif_raw: Some(vec![1, 2, 3, 4]),
464 ..Default::default()
465 };
466 md.insert(
467 MetadataKey::new(MetadataNamespace::Exif, "FlashEnergy"),
468 MetadataValue::Array(vec![
469 MetadataValue::URational(URational::new(3, 2)),
470 MetadataValue::F64(1.5),
471 ]),
472 );
473 md.insert(
474 MetadataKey::new(MetadataNamespace::Heic, "aux_count"),
475 MetadataValue::U64(2),
476 );
477
478 let json = serde_json::to_string(&md).expect("serialize");
479 let back: ImageMetadata = serde_json::from_str(&json).expect("deserialize");
480 assert_eq!(md, back, "ImageMetadata must survive a JSON round-trip");
481 }
482}