LRC-LIB-KEYWORDS: keywords from files keep their hierarchy

Exports now write names to `dc:subject` and paths to
`lr:hierarchicalSubject`, as Lightroom Classic does, but import and
sidecars read only `dc:subject`: an exported `travel|Italy|Rome` came
back as three keywords, Rome, Italy and travel. They now take the
paths of `lr:hierarchicalSubject` and the names of `dc:subject` that
aren't levels of those paths (`keywords::from_file`); without paths,
the names as they are, so LightCraft's own sidecars read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 14:46:39 -04:00
co-authored by Claude Opus 5.5
parent d62a5637d8
commit 3a145eef4e
3 changed files with 52 additions and 2 deletions
+15 -1
View File
@@ -37,7 +37,8 @@ fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option<String>) {
usage_terms: m.usage_terms.clone().unwrap_or_default(),
copyright_url: m.copyright_url.clone().unwrap_or_default(),
creator: m.artist.clone().unwrap_or_default(),
keywords: m.keywords.clone(),
// paths from `lr:hierarchicalSubject`, names from `dc:subject` that aren't their levels
keywords: lightcraft_catalog::keywords::from_file(&m.keywords, &m.hierarchical_keywords),
regions: m.regions.clone(),
};
(meta, m.capture_time.as_ref().map(|d| d.to_iso()))
@@ -587,6 +588,19 @@ impl crate::Session {
#[cfg(test)]
mod tests {
/// Keywords from a file keep their hierarchy: the paths of `lr:hierarchicalSubject`, not the
/// flat names of `dc:subject` that LightCraft's exports (and Lightroom Classic) write besides
/// them; an exported `travel|Italy|Rome` comes back as itself.
#[test]
fn imported_keywords_keep_their_hierarchy() {
let m = lightcraft_meta::Metadata {
keywords: vec!["Rome".into(), "Italy".into(), "travel".into(), "beach".into()],
hierarchical_keywords: vec!["travel|Italy|Rome".into()],
..Default::default()
};
assert_eq!(super::meta_of(&m).0.keywords, ["travel|Italy|Rome", "beach"]);
}
use super::*;
use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, Samples, encode_jpeg};
+15 -1
View File
@@ -133,7 +133,8 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result<SidecarData, S
country: m.country.clone(),
alt_text: m.alt_text.clone(),
extended_description: m.extended_description.clone(),
keywords: (!m.keywords.is_empty()).then(|| m.keywords.clone()),
keywords: (!m.keywords.is_empty() || !m.hierarchical_keywords.is_empty())
.then(|| lightcraft_catalog::keywords::from_file(&m.keywords, &m.hierarchical_keywords)),
// A sidecar that has `mwg-rs:Regions` at all (even an empty list) was written by an app that
// knows about regions, so it's authoritative: its list, empty or not, replaces the catalog's.
// One without it (most writers, LightCraft's own included, which keeps another app's
@@ -515,6 +516,19 @@ impl Session {
#[cfg(test)]
mod tests {
/// A sidecar written by Lightroom Classic (names flat, paths in `lr:hierarchicalSubject`)
/// gives the photo its keywords as paths.
#[test]
fn sidecar_keywords_keep_their_hierarchy() {
let m = lightcraft_meta::Metadata {
keywords: vec!["Lisbon".into(), "Places".into()],
hierarchical_keywords: vec!["Places|Lisbon".into()],
..Default::default()
};
let sc = super::parse_sidecar(&lightcraft_meta::write_xmp(&m, None), false).unwrap();
assert_eq!(sc.keywords, Some(vec!["Places|Lisbon".to_string()]));
}
use super::*;
use lightcraft_catalog::Meta;