use pile_config::Label; use pile_io::SyncReadBridge; use std::{ collections::HashMap, io::BufReader, sync::{Arc, OnceLock}, }; use tracing::trace; use crate::{ extract::traits::{ExtractState, ObjectExtractor}, value::{BinaryPileValue, PileValue}, }; pub struct ExifExtractor { item: BinaryPileValue, output: OnceLock>, } impl ExifExtractor { pub fn new(item: &BinaryPileValue) -> Self { Self { item: item.clone(), output: OnceLock::new(), } } async fn get_inner(&self) -> Result<&HashMap, std::io::Error> { if let Some(x) = self.output.get() { return Ok(x); } let reader = SyncReadBridge::new_current(self.item.read().await?); let raw_fields = tokio::task::spawn_blocking(move || { let mut br = BufReader::new(reader); let exif = exif::Reader::new().read_from_container(&mut br)?; let fields: Vec<(String, String)> = exif .fields() .map(|f| { ( f.tag.to_string(), f.display_value().with_unit(&exif).to_string(), ) }) .collect(); Ok::<_, exif::Error>(fields) }) .await?; let raw_fields = match raw_fields { Ok(x) => x, Err(exif::Error::Io(x)) => return Err(x), Err(error) => { trace!(message = "Could not process exif", ?error, item = ?self.item); return Ok(self.output.get_or_init(HashMap::new)); } }; let mut output: HashMap = HashMap::new(); for (tag_name, value) in raw_fields { let Some(label) = tag_to_label(&tag_name) else { continue; }; // First occurrence wins (PRIMARY IFD comes before THUMBNAIL) output .entry(label) .or_insert_with(|| PileValue::String(Arc::new(value.into()))); } return Ok(self.output.get_or_init(|| output)); } } fn tag_to_label(tag: &str) -> Option