use std::path::Path;
use std::collections::HashMap;
use exif::{Reader, Tag, Value, Exif as ExifReaderData};
#[derive(Debug, Clone, Default)]
pub struct ExifData {
pub make: Option<String>,
pub model: Option<String>,
pub lens_model: Option<String>,
pub date_time_original: Option<String>,
pub exposure_time: Option<String>,
pub f_number: Option<String>,
pub iso: Option<u32>,
pub focal_length: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub orientation: Option<u16>,
pub gps_latitude: Option<(f64, f64, f64)>,
pub gps_longitude: Option<(f64, f64, f64)>,
pub gps_altitude: Option<f64>,
pub raw_fields: HashMap<String, String>,
}
impl ExifData {
pub fn summary(&self) -> String {
let mut parts = Vec::new();
if let Some(ref make) = self.make {
parts.push(format!("相机: {}", make));
}
if let Some(ref model) = self.model {
parts.push(format!("型号: {}", model));
}
if let Some(ref lens) = self.lens_model {
parts.push(format!("镜头: {}", lens));
}
if let Some(ref date) = self.date_time_original {
parts.push(format!("拍摄时间: {}", date));
}
if let Some(ref exp) = self.exposure_time {
parts.push(format!("快门: {}", exp));
}
if let Some(ref fnum) = self.f_number {
parts.push(format!("光圈: {}", fnum));
}
if let Some(iso) = self.iso {
parts.push(format!("ISO: {}", iso));
}
if let Some(ref focal) = self.focal_length {
parts.push(format!("焦距: {}", focal));
}
if let (Some(w), Some(h)) = (self.image_width, self.image_height) {
parts.push(format!("尺寸: {}x{}", w, h));
}
parts.join(" | ")
}
pub fn has_gps(&self) -> bool {
self.gps_latitude.is_some() && self.gps_longitude.is_some()
}
pub fn gps_coordinates(&self) -> Option<(f64, f64)> {
match (self.gps_latitude, self.gps_longitude) {
(Some((d1, m1, s1)), Some((d2, m2, s2))) => {
let lat = d1 + m1 / 60.0 + s1 / 3600.0;
let lon = d2 + m2 / 60.0 + s2 / 3600.0;
Some((lat, lon))
}
_ => None,
}
}
}
pub fn extract_exif<P: AsRef<Path>>(path: P) -> Result<ExifData, ExifError> {
let path = path.as_ref();
if !path.exists() {
return Err(ExifError::FileNotFound(path.to_path_buf()));
}
let file = std::fs::File::open(path)
.map_err(|e| ExifError::Io(e))?;
let mut bufreader = std::io::BufReader::new(file);
let exifreader = Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)
.map_err(|e: exif::Error| ExifError::ParseError(e.to_string()))?;
let mut data = ExifData::default();
let mut raw_fields = HashMap::new();
for field in exif.fields() {
let tag_name = format!("{:?}", field.tag);
let value = field.display_value().to_string();
raw_fields.insert(tag_name.clone(), value.clone());
match field.tag {
Tag::Make => {
data.make = Some(value.trim_matches('"').to_string());
}
Tag::Model => {
data.model = Some(value.trim_matches('"').to_string());
}
Tag::LensModel => {
data.lens_model = Some(value.trim_matches('"').to_string());
}
Tag::DateTimeOriginal => {
data.date_time_original = Some(value.trim_matches('"').to_string());
}
Tag::ExposureTime => {
data.exposure_time = Some(value);
}
Tag::FNumber => {
data.f_number = Some(value);
}
Tag::ISOSpeed => {
if let Ok(iso) = value.parse::<u32>() {
data.iso = Some(iso);
}
}
Tag::FocalLength => {
data.focal_length = Some(value);
}
Tag::ImageWidth | Tag::PixelXDimension => {
if let Ok(w) = value.parse::<u32>() {
data.image_width = Some(w);
}
}
Tag::ImageLength | Tag::PixelYDimension => {
if let Ok(h) = value.parse::<u32>() {
data.image_height = Some(h);
}
}
Tag::Orientation => {
if let Ok(o) = value.parse::<u16>() {
data.orientation = Some(o);
}
}
_ => {}
}
}
if let Ok(gps) = extract_gps_info(&exif) {
data.gps_latitude = gps.latitude;
data.gps_longitude = gps.longitude;
data.gps_altitude = gps.altitude;
}
data.raw_fields = raw_fields;
Ok(data)
}
#[derive(Debug, Clone, Default)]
pub struct GpsInfo {
pub latitude: Option<(f64, f64, f64)>,
pub longitude: Option<(f64, f64, f64)>,
pub altitude: Option<f64>,
}
fn extract_gps_info(exif: &ExifReaderData) -> Result<GpsInfo, ExifError> {
let mut gps = GpsInfo::default();
for field in exif.fields() {
match field.tag {
Tag::GPSLatitude => {
gps.latitude = parse_gps_coordinate(&field.value);
}
Tag::GPSLongitude => {
gps.longitude = parse_gps_coordinate(&field.value);
}
Tag::GPSAltitude => {
if let Value::Rational(ref r) = field.value {
if let Some(rat) = r.first() {
let num = rat.num as f64;
let den = rat.denom as f64;
if den != 0.0 {
gps.altitude = Some(num / den);
}
}
}
}
_ => {}
}
}
Ok(gps)
}
fn parse_gps_coordinate(value: &Value) -> Option<(f64, f64, f64)> {
if let Value::Rational(ref r) = value {
if r.len() >= 3 {
let d = r[0].num as f64 / r[0].denom as f64;
let m = r[1].num as f64 / r[1].denom as f64;
let s = r[2].num as f64 / r[2].denom as f64;
return Some((d, m, s));
}
}
None
}
#[derive(Debug, thiserror::Error)]
pub enum ExifError {
#[error("File not found: {0}")]
FileNotFound(std::path::PathBuf),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse EXIF data: {0}")]
ParseError(String),
}
pub fn extract_exif_parallel<P: AsRef<Path> + Send + Sync>(
paths: &[P],
jobs: Option<usize>,
) -> Vec<(std::path::PathBuf, Result<ExifData, ExifError>)> {
use rayon::prelude::*;
let num_threads = jobs.unwrap_or_else(num_cpus::get);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.ok();
let process_fn = |path: &P| {
let path = path.as_ref();
let result = extract_exif(path);
(path.to_path_buf(), result)
};
match pool {
Some(pool) => pool.install(|| paths.par_iter().map(process_fn).collect()),
None => paths.par_iter().map(process_fn).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exif_data_default() {
let exif = ExifData::default();
assert!(exif.make.is_none());
assert!(exif.model.is_none());
assert!(!exif.has_gps());
}
#[test]
fn test_exif_data_summary() {
let exif = ExifData {
make: Some("NIKON".to_string()),
model: Some("D850".to_string()),
iso: Some(100),
..Default::default()
};
let summary = exif.summary();
assert!(summary.contains("NIKON"));
assert!(summary.contains("D850"));
assert!(summary.contains("ISO"));
}
}