#[allow(warnings)]
#[cfg(feature = "exiftool")]
pub mod exiftool {
use std::path::Path;
use anyhow::Context;
use indexmap::IndexMap;
#[cfg(target_os = "windows")]
const EXIFTOOL_BINARY: &[u8; 9383322] = include_bytes!("exiftool.exe");
pub fn run(file_path: impl AsRef<Path>) -> anyhow::Result<IndexMap<String, String>> {
let file_path = file_path.as_ref();
if !file_path.exists() {
anyhow::bail!("file not found: {}", file_path.display());
}
let output = execute(file_path)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"exiftool exited with code {:?}: {}",
output.status.code(),
stderr.trim()
);
}
parse_json_output(&output.stdout)
}
#[cfg(target_os = "windows")]
fn execute(file_path: &Path) -> anyhow::Result<std::process::Output> {
use temp_dir::TempDir;
let temp_dir = TempDir::new()?;
let exiftool_path = temp_dir.path().join("exiftool.exe");
std::fs::write(&exiftool_path, EXIFTOOL_BINARY)
.context("failed to write exiftool binary to temp directory")?;
std::process::Command::new(&exiftool_path)
.arg("-json")
.arg("-G1")
.arg("-long")
.arg("-n")
.arg(file_path)
.output()
.context("failed to execute exiftool")
}
#[cfg(not(target_os = "windows"))]
fn execute(file_path: &Path) -> anyhow::Result<std::process::Output> {
std::process::Command::new("exiftool")
.arg("-json")
.arg("-G1")
.arg("-long")
.arg("-n")
.arg(file_path)
.output()
.context("failed to execute exiftool — is it installed and on $PATH?")
}
fn parse_json_output(stdout: &[u8]) -> anyhow::Result<IndexMap<String, String>> {
let stdout_str = String::from_utf8_lossy(stdout);
let json_start = stdout_str
.find('[')
.context("exiftool output did not contain valid JSON")?;
let arr: Vec<serde_json::Map<String, serde_json::Value>> =
serde_json::from_str(&stdout_str[json_start..])
.context("failed to parse exiftool JSON output")?;
let mut map = IndexMap::new();
if let Some(obj) = arr.into_iter().next() {
for (key, value) in obj {
if key == "SourceFile" {
continue;
}
map.insert(key, json_value_to_string(value));
}
}
Ok(map)
}
fn json_value_to_string(value: serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s,
serde_json::Value::Null => String::new(),
serde_json::Value::Object(obj) => {
if let Some(val) = obj.get("val") {
return json_value_to_string(val.clone());
}
let mut parts: Vec<String> = Vec::with_capacity(obj.len());
for (k, v) in obj {
parts.push(format!("{}={}", k, json_value_to_string(v)));
}
parts.join(", ")
}
serde_json::Value::Array(arr) => arr
.into_iter()
.map(json_value_to_string)
.collect::<Vec<_>>()
.join(", "),
other => other.to_string(),
}
}
pub fn run_filtered(
file_path: impl AsRef<Path>,
filter: &str,
) -> anyhow::Result<IndexMap<String, String>> {
let all = run(file_path)?;
let filter_lower = filter.to_ascii_lowercase();
Ok(all
.into_iter()
.filter(|(k, _)| k.to_ascii_lowercase().contains(&filter_lower))
.collect())
}
pub fn get_tag(
file_path: impl AsRef<Path>,
tag: &str,
) -> anyhow::Result<Option<String>> {
Ok(run(file_path)?.get(tag).cloned())
}
}
#[cfg(feature = "exiftool")]
pub use exiftool::*;