use std::path::Path; use crate::error::Result; use crate::media_type::MediaType; use super::{ExtractedMetadata, MetadataExtractor}; pub struct ImageExtractor; impl MetadataExtractor for ImageExtractor { fn extract(&self, path: &Path) -> Result { let mut meta = ExtractedMetadata::default(); let file = std::fs::File::open(path)?; let mut buf_reader = std::io::BufReader::new(&file); let exif_data = match exif::Reader::new().read_from_container(&mut buf_reader) { Ok(exif) => exif, Err(_) => return Ok(meta), }; // Image dimensions if let Some(width) = exif_data .get_field(exif::Tag::PixelXDimension, exif::In::PRIMARY) .or_else(|| exif_data.get_field(exif::Tag::ImageWidth, exif::In::PRIMARY)) && let Some(w) = field_to_u32(width) { meta.extra.insert("width".to_string(), w.to_string()); } if let Some(height) = exif_data .get_field(exif::Tag::PixelYDimension, exif::In::PRIMARY) .or_else(|| exif_data.get_field(exif::Tag::ImageLength, exif::In::PRIMARY)) && let Some(h) = field_to_u32(height) { meta.extra.insert("height".to_string(), h.to_string()); } // Camera make and model if let Some(make) = exif_data.get_field(exif::Tag::Make, exif::In::PRIMARY) { let val = make.display_value().to_string(); if !val.is_empty() { meta.extra.insert("camera_make".to_string(), val); } } if let Some(model) = exif_data.get_field(exif::Tag::Model, exif::In::PRIMARY) { let val = model.display_value().to_string(); if !val.is_empty() { meta.extra.insert("camera_model".to_string(), val); } } // Date taken if let Some(date) = exif_data .get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY) .or_else(|| exif_data.get_field(exif::Tag::DateTime, exif::In::PRIMARY)) { let val = date.display_value().to_string(); if !val.is_empty() { meta.extra.insert("date_taken".to_string(), val); } } // GPS coordinates if let (Some(lat), Some(lat_ref), Some(lon), Some(lon_ref)) = ( exif_data.get_field(exif::Tag::GPSLatitude, exif::In::PRIMARY), exif_data.get_field(exif::Tag::GPSLatitudeRef, exif::In::PRIMARY), exif_data.get_field(exif::Tag::GPSLongitude, exif::In::PRIMARY), exif_data.get_field(exif::Tag::GPSLongitudeRef, exif::In::PRIMARY), ) && let (Some(lat_val), Some(lon_val)) = (dms_to_decimal(lat, lat_ref), dms_to_decimal(lon, lon_ref)) { meta.extra .insert("gps_latitude".to_string(), format!("{lat_val:.6}")); meta.extra .insert("gps_longitude".to_string(), format!("{lon_val:.6}")); } // Exposure info if let Some(iso) = exif_data.get_field(exif::Tag::PhotographicSensitivity, exif::In::PRIMARY) { let val = iso.display_value().to_string(); if !val.is_empty() { meta.extra.insert("iso".to_string(), val); } } if let Some(exposure) = exif_data.get_field(exif::Tag::ExposureTime, exif::In::PRIMARY) { let val = exposure.display_value().to_string(); if !val.is_empty() { meta.extra.insert("exposure_time".to_string(), val); } } if let Some(aperture) = exif_data.get_field(exif::Tag::FNumber, exif::In::PRIMARY) { let val = aperture.display_value().to_string(); if !val.is_empty() { meta.extra.insert("f_number".to_string(), val); } } if let Some(focal) = exif_data.get_field(exif::Tag::FocalLength, exif::In::PRIMARY) { let val = focal.display_value().to_string(); if !val.is_empty() { meta.extra.insert("focal_length".to_string(), val); } } // Lens model if let Some(lens) = exif_data.get_field(exif::Tag::LensModel, exif::In::PRIMARY) { let val = lens.display_value().to_string(); if !val.is_empty() && val != "\"\"" { meta.extra .insert("lens_model".to_string(), val.trim_matches('"').to_string()); } } // Flash if let Some(flash) = exif_data.get_field(exif::Tag::Flash, exif::In::PRIMARY) { let val = flash.display_value().to_string(); if !val.is_empty() { meta.extra.insert("flash".to_string(), val); } } // Orientation if let Some(orientation) = exif_data.get_field(exif::Tag::Orientation, exif::In::PRIMARY) { let val = orientation.display_value().to_string(); if !val.is_empty() { meta.extra.insert("orientation".to_string(), val); } } // Software if let Some(software) = exif_data.get_field(exif::Tag::Software, exif::In::PRIMARY) { let val = software.display_value().to_string(); if !val.is_empty() { meta.extra.insert("software".to_string(), val); } } // Image description as title if let Some(desc) = exif_data.get_field(exif::Tag::ImageDescription, exif::In::PRIMARY) { let val = desc.display_value().to_string(); if !val.is_empty() && val != "\"\"" { meta.title = Some(val.trim_matches('"').to_string()); } } // Artist if let Some(artist) = exif_data.get_field(exif::Tag::Artist, exif::In::PRIMARY) { let val = artist.display_value().to_string(); if !val.is_empty() && val != "\"\"" { meta.artist = Some(val.trim_matches('"').to_string()); } } // Copyright as description if let Some(copyright) = exif_data.get_field(exif::Tag::Copyright, exif::In::PRIMARY) { let val = copyright.display_value().to_string(); if !val.is_empty() && val != "\"\"" { meta.description = Some(val.trim_matches('"').to_string()); } } Ok(meta) } fn supported_types(&self) -> &[MediaType] { &[ MediaType::Jpeg, MediaType::Png, MediaType::Gif, MediaType::Webp, MediaType::Avif, MediaType::Tiff, MediaType::Bmp, // RAW formats (TIFF-based, kamadak-exif handles these) MediaType::Cr2, MediaType::Nef, MediaType::Arw, MediaType::Dng, MediaType::Orf, MediaType::Rw2, // HEIC MediaType::Heic, ] } } fn field_to_u32(field: &exif::Field) -> Option { match &field.value { exif::Value::Long(v) => v.first().copied(), exif::Value::Short(v) => v.first().map(|&x| x as u32), _ => None, } } fn dms_to_decimal(dms_field: &exif::Field, ref_field: &exif::Field) -> Option { if let exif::Value::Rational(ref rationals) = dms_field.value && rationals.len() >= 3 { let degrees = rationals[0].to_f64(); let minutes = rationals[1].to_f64(); let seconds = rationals[2].to_f64(); let mut decimal = degrees + minutes / 60.0 + seconds / 3600.0; let ref_str = ref_field.display_value().to_string(); if ref_str.contains('S') || ref_str.contains('W') { decimal = -decimal; } return Some(decimal); } None }