EXIF GPS write: POST /image/exif/gps via exiftool
New endpoint accepts {path, library, latitude, longitude} and shells
out to exiftool to write GPSLatitude/GPSLongitude (with N/S, E/W refs)
into the file's EXIF in place. After the write, the handler
re-extracts EXIF and updates the image_exif row so the DB stays in
sync — the response carries the updated metadata block in one
round-trip. Falls through to store_exif if the row is missing.
`exif::write_gps` is the small helper. `-overwrite_original` so no
.orig sidecar is left behind. Validates lat/lon range + supports_exif
before spawning exiftool. Format support matches the existing read
path (JPEG / TIFF / RAW / HEIF / PNG / WebP) — videos still need a
different writer and aren't covered.
Apollo's "+ PIN" carousel button (separate commit on the Apollo side)
calls this through /api/photos/exif/gps. Drive-by: cargo fmt one-line
collapse on apollo_client.rs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
src/exif.rs
46
src/exif.rs
@@ -160,6 +160,52 @@ pub fn extract_embedded_jpeg_preview(path: &Path) -> Option<Vec<u8>> {
|
||||
.max_by_key(|v| v.len())
|
||||
}
|
||||
|
||||
/// Write GPS lat/lon into the file's EXIF in place via exiftool. Touches
|
||||
/// nothing else — camera, dates, MakerNote, etc. all stay as-is. Uses
|
||||
/// `-overwrite_original` so no `.orig` sidecar is left behind (the
|
||||
/// caller's responsibility to back up the file system if they want
|
||||
/// rollback). Returns Err if exiftool isn't on PATH, the file format
|
||||
/// doesn't support EXIF, lat/lon are out of range, or exiftool prints
|
||||
/// to stderr.
|
||||
///
|
||||
/// We pass lat/lon as positive decimal numbers and let the *Ref tags
|
||||
/// carry the sign (N/S, E/W). exiftool happily accepts signed decimals
|
||||
/// too, but the explicit ref form is unambiguous across exiftool
|
||||
/// versions and matches what cameras write.
|
||||
pub fn write_gps(path: &Path, lat: f64, lon: f64) -> Result<()> {
|
||||
if !supports_exif(path) {
|
||||
return Err(anyhow!(
|
||||
"Format does not support EXIF GPS write: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
|
||||
return Err(anyhow!("GPS coordinates out of range: {}, {}", lat, lon));
|
||||
}
|
||||
let lat_ref = if lat >= 0.0 { "N" } else { "S" };
|
||||
let lon_ref = if lon >= 0.0 { "E" } else { "W" };
|
||||
let lat_abs = lat.abs();
|
||||
let lon_abs = lon.abs();
|
||||
let output = Command::new("exiftool")
|
||||
.arg("-overwrite_original")
|
||||
.arg(format!("-GPSLatitude={}", lat_abs))
|
||||
.arg(format!("-GPSLatitudeRef={}", lat_ref))
|
||||
.arg(format!("-GPSLongitude={}", lon_abs))
|
||||
.arg(format!("-GPSLongitudeRef={}", lon_ref))
|
||||
.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 failed (exit {}): {}",
|
||||
output.status.code().unwrap_or(-1),
|
||||
stderr.trim()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn supports_exif(path: &Path) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
let ext_lower = ext.to_string_lossy().to_lowercase();
|
||||
|
||||
Reference in New Issue
Block a user