exif: GET /image/exif/full — exiftool dump for the DETAILS modal
The curated `image_exif` columns are a small slice of what exiftool can read (camera/lens/GPS/capture/dates). Apollo's DETAILS modal wants to surface everything — white balance, metering, MakerNotes, IPTC, ICC profile, Composite tags, the lot — for an operator inspecting a photo's provenance. `read_full_exif_via_exiftool(path)` shells out to `exiftool -j -G -n`: JSON output, group-prefixed keys (`EXIF:Make`, `MakerNotes:LensInfo`), numeric values (callers can reformat). Spawned via web::block to keep it off the actix worker — RAW with rich MakerNotes can take a few seconds. The endpoint is on-demand only; the indexer / file watcher does NOT call it. Falls back to 503 with a clear message when exiftool isn't on PATH so Apollo can render an "install exiftool" hint. Multi-library union resolution mirrors set_image_gps / get_file_metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
src/exif.rs
47
src/exif.rs
@@ -71,6 +71,53 @@ fn read_jpeg_at_ifd(exif: &exif::Exif, path: &Path, ifd: In) -> Option<Vec<u8>>
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Shell out to `exiftool -j -G -n <path>` and return the per-file tag map.
|
||||
///
|
||||
/// `-j` requests JSON; the response is always an array of one element per
|
||||
/// input path. `-G` prefixes each key with the group name (`EXIF:Make`,
|
||||
/// `MakerNotes:LensInfo`, `File:FileSize`, …) so a UI can group the dump.
|
||||
/// `-n` returns numeric / raw values rather than exiftool's pretty-printed
|
||||
/// human strings, which keeps the output stable for clients that want to
|
||||
/// reformat (e.g. divide a focal-length numerator/denominator).
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(Some(value))` — the parsed object for this file.
|
||||
/// - `Ok(None)` — exiftool ran but the array was empty / not an object.
|
||||
/// - `Err(_)` — exiftool isn't on PATH, the spawn failed, or its stderr
|
||||
/// indicates an unsupported file. Caller surfaces a 503 / 422.
|
||||
///
|
||||
/// Used by `GET /image/exif/full` to power Apollo's DETAILS modal "FULL
|
||||
/// EXIF" pane. Per-file shell-out is fine for this on-demand surface;
|
||||
/// the indexer does NOT call this on the hot path (kamadak-exif covers
|
||||
/// the indexed columns; exiftool is the slow-path preview helper).
|
||||
pub fn read_full_exif_via_exiftool(path: &Path) -> Result<Option<serde_json::Value>> {
|
||||
let output = Command::new("exiftool")
|
||||
.arg("-j")
|
||||
.arg("-G")
|
||||
.arg("-n")
|
||||
.arg(path)
|
||||
.output()
|
||||
.map_err(|e| anyhow!("exiftool spawn failed (is it on PATH?): {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!(
|
||||
"exiftool exited with {}: {}",
|
||||
output.status,
|
||||
stderr.trim()
|
||||
));
|
||||
}
|
||||
|
||||
let parsed: serde_json::Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|e| anyhow!("exiftool returned non-JSON output: {}", e))?;
|
||||
|
||||
// `-j` always wraps the result in an array — pull out the first object.
|
||||
let arr = parsed
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!("expected JSON array from exiftool -j"))?;
|
||||
Ok(arr.first().cloned())
|
||||
}
|
||||
|
||||
/// Tags exiftool exposes for embedded JPEG previews, in priority order. The
|
||||
/// largest valid JPEG returned by any of them wins. Different camera makers
|
||||
/// stash their largest preview under different names: Nikon's full-res
|
||||
|
||||
Reference in New Issue
Block a user