Skip to main content

flashraw_core/
exif.rs

1//! Standard EXIF/TIFF metadata extraction into [`RawMetadata`].
2//!
3//! Only tags that are cheap to read are extracted here; vendor maker notes are
4//! parsed by the per-format modules.
5
6use crate::metadata::{GpsInfo, RawMetadata};
7use crate::orientation::{OrientationMetadata, OrientationPolicy};
8use crate::tiff::{Ifd, tags};
9use crate::types::Orientation;
10
11/// Fill basic camera information from a TIFF IFD (typically IFD0).
12pub fn read_ifd0(ifd: &Ifd, meta: &mut RawMetadata) {
13    if meta.make.is_empty() {
14        if let Some(s) = ifd.string(tags::MAKE) {
15            meta.make = s;
16        }
17    }
18    if meta.model.is_empty() {
19        if let Some(s) = ifd.string(tags::MODEL) {
20            meta.model = s;
21        }
22    }
23    if let Some(o) = ifd.u32(tags::ORIENTATION) {
24        meta.orientation = Orientation::from_exif(o);
25        meta.orientation_info =
26            OrientationMetadata::resolve(Orientation::from_exif_checked(o), None, None, OrientationPolicy::ExifOnly);
27    }
28    if meta.software.is_none() {
29        meta.software = ifd.string(tags::SOFTWARE);
30    }
31    if meta.artist.is_none() {
32        meta.artist = ifd.string(tags::ARTIST);
33    }
34    if meta.copyright.is_none() {
35        meta.copyright = ifd.string(tags::COPYRIGHT);
36    }
37    if meta.capture_time.is_none() {
38        meta.capture_time = ifd.string(tags::DATE_TIME).and_then(|s| normalize_datetime(&s, None, None));
39    }
40    if let Some(e) = &ifd.exif {
41        read_exif(e, meta);
42    }
43    if let Some(g) = &ifd.gps {
44        meta.gps = read_gps(g);
45    }
46    // TIFF-EP cameras sometimes store exposure tags directly in IFD0.
47    read_exif(ifd, meta);
48}
49
50/// Fill exposure/lens/time information from an EXIF IFD.
51pub fn read_exif(e: &Ifd, meta: &mut RawMetadata) {
52    let x = &mut meta.exposure;
53    if x.exposure_time.is_none() {
54        x.exposure_time = e.f64(tags::EXPOSURE_TIME).filter(|v| *v > 0.0);
55    }
56    if x.f_number.is_none() {
57        x.f_number = e.f64(tags::F_NUMBER).map(|v| v as f32).filter(|v| *v > 0.0);
58    }
59    if x.iso.is_none() {
60        x.iso = e.get(tags::ISO).and_then(|v| v.u32(0)).filter(|v| *v > 0);
61    }
62    if x.focal_length.is_none() {
63        x.focal_length = e.f64(tags::FOCAL_LENGTH).map(|v| v as f32).filter(|v| *v > 0.0);
64    }
65    if x.focal_length_35mm.is_none() {
66        x.focal_length_35mm = e.u32(tags::FOCAL_LENGTH_35MM).filter(|v| *v > 0);
67    }
68    if x.exposure_bias.is_none() {
69        x.exposure_bias = e.f64(tags::EXPOSURE_BIAS).map(|v| v as f32);
70    }
71    if x.flash.is_none() {
72        x.flash = e.u32(tags::FLASH).map(|v| v as u16);
73    }
74    if x.metering_mode.is_none() {
75        x.metering_mode = e.u32(tags::METERING_MODE).map(|v| v as u16);
76    }
77    if x.exposure_program.is_none() {
78        x.exposure_program = e.u32(tags::EXPOSURE_PROGRAM).map(|v| v as u16);
79    }
80    if let Some(s) = e.string(tags::DATE_TIME_ORIGINAL) {
81        let sub = e.string(tags::SUBSEC_TIME_ORIGINAL);
82        let off = e.string(tags::OFFSET_TIME_ORIGINAL);
83        if let Some(t) = normalize_datetime(&s, sub.as_deref(), off.as_deref()) {
84            meta.capture_time = Some(t);
85        }
86    }
87    if meta.serial.is_none() {
88        meta.serial = e.string(tags::BODY_SERIAL);
89    }
90    // The size of the image the camera itself renders (its JPEG), when the raw's EXIF
91    // mirrors it.
92    if meta.image_size().is_none() {
93        if let (Some(w), Some(h)) = (e.u32(tags::PIXEL_X_DIMENSION), e.u32(tags::PIXEL_Y_DIMENSION)) {
94            meta.set_image_size(w, h);
95        }
96    }
97    let l = &mut meta.lens;
98    if l.make.is_none() {
99        l.make = e.string(tags::LENS_MAKE);
100    }
101    if l.model.is_none() {
102        l.model = e.string(tags::LENS_MODEL);
103    }
104    if l.serial.is_none() {
105        l.serial = e.string(tags::LENS_SERIAL);
106    }
107    if l.focal_range.is_none() {
108        if let Some(spec) = e.get(tags::LENS_SPEC) {
109            let v = spec.f64s();
110            if v.len() >= 4 && v[0] > 0.0 {
111                l.focal_range = Some((v[0] as f32, v[1] as f32));
112                if v[2] > 0.0 {
113                    l.aperture_range = Some((v[2] as f32, v[3] as f32));
114                }
115            }
116        }
117    }
118}
119
120/// Parse a GPS IFD.
121pub fn read_gps(g: &Ifd) -> Option<GpsInfo> {
122    let dms = |tag: u16| -> Option<f64> {
123        let v = g.get(tag)?.f64s();
124        (v.len() >= 3).then(|| v[0] + v[1] / 60.0 + v[2] / 3600.0)
125    };
126    let mut info = GpsInfo::default();
127    if let Some(lat) = dms(0x0002) {
128        let r = g.string(0x0001).unwrap_or_default();
129        info.latitude = Some(if r.starts_with('S') { -lat } else { lat });
130    }
131    if let Some(lon) = dms(0x0004) {
132        let r = g.string(0x0003).unwrap_or_default();
133        info.longitude = Some(if r.starts_with('W') { -lon } else { lon });
134    }
135    if let Some(alt) = g.f64(0x0006) {
136        let below = g.get(0x0005).and_then(|e| e.u32(0)).unwrap_or(0) == 1;
137        info.altitude = Some(if below { -alt } else { alt });
138    }
139    if let (Some(d), Some(t)) = (g.string(0x001D), g.get(0x0007)) {
140        let t = t.f64s();
141        if t.len() >= 3 {
142            info.timestamp =
143                Some(format!("{}T{:02}:{:02}:{:02}Z", d.replace(':', "-"), t[0] as u32, t[1] as u32, t[2] as u32));
144        }
145    }
146    (info.latitude.is_some() || info.longitude.is_some()).then_some(info)
147}
148
149/// Convert an EXIF `YYYY:MM:DD HH:MM:SS` timestamp into ISO 8601.
150pub fn normalize_datetime(s: &str, subsec: Option<&str>, offset: Option<&str>) -> Option<String> {
151    let s = s.trim();
152    let b = s.as_bytes();
153    if b.len() < 19 || !b[..4].iter().all(|c| c.is_ascii_digit()) || s.starts_with("0000") {
154        return None;
155    }
156    let mut out = format!("{}-{}-{}T{}", &s[0..4], &s[5..7], &s[8..10], &s[11..19]);
157    if let Some(ss) = subsec.map(str::trim).filter(|v| !v.is_empty() && v.bytes().all(|c| c.is_ascii_digit())) {
158        out.push('.');
159        out.push_str(ss);
160    }
161    if let Some(o) = offset.map(str::trim).filter(|v| v.len() == 6 && (v.starts_with('+') || v.starts_with('-'))) {
162        out.push_str(o);
163    }
164    Some(out)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn datetime() {
173        assert_eq!(
174            normalize_datetime("2021:03:04 05:06:07", Some("12"), Some("+02:00")).unwrap(),
175            "2021-03-04T05:06:07.12+02:00"
176        );
177        assert!(normalize_datetime("0000:00:00 00:00:00", None, None).is_none());
178        assert!(normalize_datetime("bad", None, None).is_none());
179    }
180}