1use crate::types::ExifMeta;
2use exif::{In, Reader as ExifReader, Tag, Value};
3use std::fs::File;
4use std::io::BufReader;
5use std::path::Path;
6
7pub fn read_exif(path: &Path) -> Option<ExifMeta> {
9 let file = File::open(path).ok()?;
10 let mut bufreader = BufReader::new(&file);
11 let exifreader = ExifReader::new();
12 let exif = exifreader.read_from_container(&mut bufreader).ok()?;
13
14 let mut info = ExifMeta::default();
15
16 let make = exif
17 .get_field(Tag::Make, In::PRIMARY)
18 .map(|f| f.display_value().to_string());
19 let model = exif
20 .get_field(Tag::Model, In::PRIMARY)
21 .map(|f| f.display_value().to_string());
22 info.camera = match (make, model) {
23 (Some(m), Some(md)) if !m.is_empty() && !md.is_empty() => Some(format!("{m} {md}")),
24 (Some(m), _) if !m.is_empty() => Some(m),
25 (_, Some(md)) if !md.is_empty() => Some(md),
26 _ => None,
27 };
28
29 if let Some(f) = exif.get_field(Tag::LensModel, In::PRIMARY) {
30 let s = f.display_value().to_string();
31 if !s.is_empty() {
32 info.lens = Some(s);
33 }
34 }
35
36 let lat_ref = exif
37 .get_field(Tag::GPSLatitudeRef, In::PRIMARY)
38 .map(|f| f.display_value().to_string());
39 let lon_ref = exif
40 .get_field(Tag::GPSLongitudeRef, In::PRIMARY)
41 .map(|f| f.display_value().to_string());
42 if let Some(f) = exif.get_field(Tag::GPSLatitude, In::PRIMARY) {
43 info.gps_lat = parse_gps_decimal(f.display_value().to_string(), lat_ref.as_deref());
44 }
45 if let Some(f) = exif.get_field(Tag::GPSLongitude, In::PRIMARY) {
46 info.gps_lon = parse_gps_decimal(f.display_value().to_string(), lon_ref.as_deref());
47 }
48
49 for field in exif.fields() {
50 if is_known_tag(field.tag) {
51 continue;
52 }
53 let key = format!("{:?}", field.tag);
54 info.extra.insert(key, exif_value_to_json(&field.value));
55 }
56
57 if info.camera.is_none()
58 && info.lens.is_none()
59 && info.gps_lat.is_none()
60 && info.gps_lon.is_none()
61 && info.extra.is_empty()
62 {
63 None
64 } else {
65 Some(info)
66 }
67}
68
69fn is_known_tag(tag: Tag) -> bool {
70 matches!(
71 tag,
72 Tag::Make
73 | Tag::Model
74 | Tag::LensModel
75 | Tag::GPSLatitude
76 | Tag::GPSLongitude
77 | Tag::GPSLatitudeRef
78 | Tag::GPSLongitudeRef
79 )
80}
81
82fn parse_gps_decimal(text: String, reference: Option<&str>) -> Option<f64> {
83 let filtered = text
84 .replace("deg", " ")
85 .replace("min", " ")
86 .replace("sec", " ")
87 .replace(['"', '\''], " ");
88 let mut nums = filtered
89 .split_whitespace()
90 .filter_map(|p| p.parse::<f64>().ok());
91 let deg = nums.next()?;
92 let min = nums.next().unwrap_or(0.0);
93 let sec = nums.next().unwrap_or(0.0);
94 let mut decimal = deg + (min / 60.0) + (sec / 3600.0);
95 if let Some(reference) = reference {
96 let upper = reference.trim().to_ascii_uppercase();
97 if upper.contains('S') || upper.contains('W') {
98 decimal *= -1.0;
99 }
100 }
101 Some(decimal)
102}
103
104fn exif_value_to_json(value: &Value) -> serde_json::Value {
105 match value {
106 Value::Byte(v) => serde_json::json!(v),
107 Value::Ascii(v) => serde_json::json!(v
108 .iter()
109 .map(|s| String::from_utf8_lossy(s).to_string())
110 .collect::<Vec<_>>()),
111 Value::Short(v) => serde_json::json!(v),
112 Value::Long(v) => serde_json::json!(v),
113 Value::Rational(v) => serde_json::json!(v
114 .iter()
115 .map(|r| {
116 if r.denom == 0 {
117 serde_json::Value::Null
118 } else {
119 serde_json::json!(r.num as f64 / r.denom as f64)
120 }
121 })
122 .collect::<Vec<_>>()),
123 Value::SByte(v) => serde_json::json!(v),
124 Value::Undefined(v, _) => serde_json::json!(v),
125 Value::SShort(v) => serde_json::json!(v),
126 Value::SLong(v) => serde_json::json!(v),
127 Value::SRational(v) => serde_json::json!(v
128 .iter()
129 .map(|r| {
130 if r.denom == 0 {
131 serde_json::Value::Null
132 } else {
133 serde_json::json!(r.num as f64 / r.denom as f64)
134 }
135 })
136 .collect::<Vec<_>>()),
137 Value::Float(v) => serde_json::json!(v),
138 Value::Double(v) => serde_json::json!(v),
139 Value::Unknown(_, _, _) => serde_json::Value::Null,
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::parse_gps_decimal;
146
147 #[test]
148 fn gps_ref_south_and_west_are_negative() {
149 let lat = parse_gps_decimal("40 deg 26 min 46 sec".to_string(), Some("S")).unwrap();
150 let lon = parse_gps_decimal("79 deg 58 min 56 sec".to_string(), Some("W")).unwrap();
151 assert!(lat < 0.0);
152 assert!(lon < 0.0);
153 }
154
155 #[test]
156 fn gps_ref_north_and_east_stay_positive() {
157 let lat = parse_gps_decimal("40 deg 26 min 46 sec".to_string(), Some("N")).unwrap();
158 let lon = parse_gps_decimal("79 deg 58 min 56 sec".to_string(), Some("E")).unwrap();
159 assert!(lat > 0.0);
160 assert!(lon > 0.0);
161 }
162}