Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
213 lines
7.7 KiB
Rust
213 lines
7.7 KiB
Rust
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<ExtractedMetadata> {
|
|
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<u32> {
|
|
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<f64> {
|
|
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
|
|
}
|