use anyhow::Result;
use exif::{In, Reader, Tag};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use super::{Confidence, Signal, SignalBuilder, SignalSource};
use crate::known_tools;
const CAMERA_TAGS: &[Tag] = &[
Tag::Make,
Tag::Model,
Tag::LensModel,
Tag::FocalLength,
Tag::FNumber,
Tag::ExposureTime,
Tag::PhotographicSensitivity,
Tag::Flash,
Tag::MeteringMode,
Tag::WhiteBalance,
];
pub fn detect(path: &Path) -> Result<Vec<Signal>> {
let file = File::open(path)?;
let exif = match Reader::new().read_from_container(&mut BufReader::new(file)) {
Ok(e) => e,
Err(_) => return Ok(vec![]),
};
let mut signals = Vec::new();
let mut software_matched = false;
if let Some(field) = exif.get_field(Tag::Software, In::PRIMARY) {
let sw = field.display_value().to_string().replace('"', "");
if let Some(tool_name) = known_tools::match_ai_tool(&sw) {
signals.push(
SignalBuilder::new(SignalSource::Exif, Confidence::Low, "signal_exif_software")
.param("value", &sw)
.tool(tool_name)
.detail("Software", &sw)
.build(),
);
software_matched = true;
}
}
for tag in &[Tag::Make, Tag::Model] {
if let Some(field) = exif.get_field(*tag, In::PRIMARY) {
let val = field.display_value().to_string().replace('"', "");
if let Some(tool_name) = known_tools::match_ai_tool(&val) {
signals.push(
SignalBuilder::new(
SignalSource::Exif,
Confidence::Low,
"signal_exif_tag_value",
)
.param("tag", tag.to_string())
.param("value", &val)
.tool(tool_name)
.detail(tag.to_string(), &val)
.build(),
);
software_matched = true;
}
}
}
for tag in &[Tag::ImageDescription, Tag::UserComment] {
if let Some(field) = exif.get_field(*tag, In::PRIMARY) {
let val = field.display_value().to_string().replace('"', "");
if let Some(tool_name) = known_tools::match_ai_tool(&val) {
signals.push(
SignalBuilder::new(
SignalSource::Exif,
Confidence::Low,
"signal_exif_tag_references_ai",
)
.param("tag", tag.to_string())
.tool(tool_name)
.detail(tag.to_string(), &val)
.build(),
);
software_matched = true;
}
}
}
if let Some(field) = exif.get_field(Tag::Artist, In::PRIMARY) {
let val = field.display_value().to_string().replace('"', "");
let is_hex_hash = val.len() >= 32 && val.chars().all(|c| c.is_ascii_hexdigit() || c == '-');
if is_hex_hash {
let prefix = &val[..val.len().min(16)];
signals.push(
SignalBuilder::new(
SignalSource::Exif,
Confidence::Low,
"signal_exif_artist_hash",
)
.param("value", prefix)
.detail("Artist", &val)
.build(),
);
software_matched = true;
}
}
if software_matched {
let camera_tag_count = CAMERA_TAGS
.iter()
.filter(|&&tag| exif.get_field(tag, In::PRIMARY).is_some())
.count();
if camera_tag_count == 0 {
signals.push(
SignalBuilder::new(SignalSource::Exif, Confidence::Low, "signal_exif_no_camera")
.detail("camera_tags_present", "0")
.build(),
);
}
}
Ok(signals)
}
pub fn dump_info(path: &Path) -> Result<Vec<(String, String)>> {
let file = File::open(path)?;
let exif = match Reader::new().read_from_container(&mut BufReader::new(file)) {
Ok(e) => e,
Err(_) => return Ok(vec![]),
};
let mut fields = Vec::new();
for field in exif.fields() {
fields.push((format!("{}", field.tag), field.display_value().to_string()));
}
Ok(fields)
}