Files
lightcraft/crates/engine/src/cmd/xmp.rs
T
Brandon ThomasandClaude Opus 5.5 8af27c11b1 M10.8: XMP sidecars — save/read metadata + develop settings, auto-write, crs: interchange
- crates/engine/src/sidecar.rs: <stem>.xmp (default) or <file>.xmp next to originals with rating,
  label, title, caption, copyright, creator, keywords, capture time/GPS, pick flag + full develop
  settings in lc:settings; atomic writes; read on import (sidecar wins for metadata, edits restored)
  and via photo.readMetadataFromFile (one undo step); raw/DNG embedded XMP used without a sidecar.
- Commands: photo.saveMetadataToFile (Cmd+S), photo.readMetadataFromFile, library.xmpPreferences
  {autoWrite, naming}, library.toggleAutoWriteXmp; prefs persisted in the library's prefs.json.
  Auto-write rewrites sidecars after each committed change (not mid-drag; undo/redo too).
- crates/engine/src/crs.rs: crs: develop fields (PV2012 light, WB abs/incremental, presence, HSL,
  B&W mix, parametric + point curves, colour grading, sharpening, NR, vignette, grain, optics,
  geometry, crop) → partial settings; documented in docs/xmp-interop.md.
- meta: write_xmp_lc for extra lc: properties; crs namespace canonicalised on read (never written).
- Tests: packet round trip, crs mapping with hand-written snippets, save → delete library →
  re-import restores edits, auto-write + prefs persistence, foreign sidecar, DNG-embedded XMP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:36:19 -04:00

94 lines
3.2 KiB
Rust

//! XMP sidecar commands: save/read metadata to/from files, sidecar preferences.
use lightcraft_catalog::Op;
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, cmd, has_selection, str_param};
use crate::sidecar::SidecarNaming;
use crate::{Result, Session};
fn save(s: &mut Session, p: &Value) -> Result<Value> {
let mut written = Vec::new();
let mut failed = Vec::new();
for id in s.targets(p) {
match s.save_sidecar(id) {
Ok(path) => written.push(path.display().to_string()),
Err(e) => failed.push(json!({"id": id.0, "error": e.to_string()})),
}
}
Ok(json!({"written": written, "failed": failed}))
}
fn read(s: &mut Session, p: &Value) -> Result<Value> {
let mut ops = Vec::new();
let mut read = Vec::new();
let mut failed = Vec::new();
for id in s.targets(p) {
match s.read_sidecar_op(id) {
Ok(Some((op, from))) => {
ops.push(op);
read.push(json!({"id": id.0, "from": from.display().to_string()}));
}
Ok(None) => failed.push(json!({"id": id.0, "error": "no XMP sidecar or embedded XMP"})),
Err(e) => failed.push(json!({"id": id.0, "error": e.to_string()})),
}
}
if !ops.is_empty() {
s.commit("Read Metadata from File", Op::Batch { ops })?;
}
Ok(json!({"read": read, "failed": failed}))
}
fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(a) = p.get("autoWrite").and_then(Value::as_bool) {
s.xmp.auto_write = a;
}
if let Some(n) = str_param(p, "naming") {
s.xmp.naming = match n {
"stem" => SidecarNaming::Stem,
"full" => SidecarNaming::Full,
other => return Err(bad("library.xmpPreferences", format!("unknown naming `{other}` (stem|full)"))),
};
}
if p.as_object().is_some_and(|o| !o.is_empty()) {
s.save_prefs()?;
}
Ok(serde_json::to_value(s.xmp).unwrap_or_default())
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"photo.saveMetadataToFile",
"Save Metadata to File",
["Photo"],
Some("Cmd+S"),
"{ids?} — writes each photo's XMP sidecar (metadata + develop settings) next to the original → {written: [path], failed}",
has_selection,
save
),
cmd!(
"photo.readMetadataFromFile",
"Read Metadata from File",
["Photo"],
None,
"{ids?} — reads each photo's XMP sidecar (or a raw/DNG's embedded XMP): metadata and develop settings, one undo step → {read, failed}",
has_selection,
read
),
cmd!(
"library.xmpPreferences",
"XMP Sidecar Preferences",
[],
None,
"{autoWrite?: bool, naming?: stem (IMG_1.xmp, default) | full (IMG_1.CR3.xmp)} → current preferences (saved with the library)",
always,
prefs
),
cmd!("library.toggleAutoWriteXmp", "Automatically Write Changes into XMP", ["File"], None, "{} → {autoWrite}", always, |s, _| {
let on = !s.xmp.auto_write;
prefs(s, &json!({"autoWrite": on}))
}),
]
}