initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
213
crates/pinakes-core/src/metadata/image.rs
Normal file
213
crates/pinakes-core/src/metadata/image.rs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue