From fcef4c0fc9d88643b33c231020fe90d1a9bee5e4 Mon Sep 17 00:00:00 2001 From: rock-meister <303256+rock-meister@users.noreply.github.com> Date: Fri, 9 Oct 2026 05:15:52 -0500 Subject: [PATCH] LRC-LIB-CATALOG-IMPORT: import catalog crops that have no HasCrop Lightroom Classic catalogs store the crop edges and straighten angle in their develop settings but, unlike XMP, never a HasCrop flag. The shared crs mapper only applies a crop when crs:HasCrop is True, so every crop in an imported catalog was dropped and reported as unmapped. Infer HasCrop in the catalog path when any crop field is present. The XMP path is unchanged: there HasCrop="False" with crop fields means the crop is switched off. Refs #483 Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/lightroom_catalog.rs | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/lightroom_catalog.rs b/crates/engine/src/lightroom_catalog.rs index 3c510fded..def9e4632 100644 --- a/crates/engine/src/lightroom_catalog.rs +++ b/crates/engine/src/lightroom_catalog.rs @@ -480,7 +480,13 @@ fn ensure_read_active(cancel: &AtomicBool) -> Result<(), String> { fn mapped_settings(text: &str, raw: bool, aspect: f64) -> Result<(Value, Vec), String> { let mut root = crate::preset_import::parse_lua(text)?; let deferred = strip_lua_sentinels(&mut root); - let (props, values) = crate::preset_import::lua_settings_props(&root)?; + let (mut props, values) = crate::preset_import::lua_settings_props(&root)?; + // A catalog's develop settings carry the crop edges and angle but no `HasCrop` (XMP has it): + // any crop field means the photo is cropped. + const CROP: [&str; 5] = ["crs:CropLeft", "crs:CropTop", "crs:CropRight", "crs:CropBottom", "crs:CropAngle"]; + if !props.contains_key("crs:HasCrop") && CROP.iter().any(|k| props.contains_key(*k)) { + props.insert("crs:HasCrop".into(), vec!["True".into()]); + } let (partial, mut unknown) = crate::crs::to_partial_report(&props, Some(&values), Some(raw), aspect); if deferred > 0 { unknown.push("Deferred Lightroom adjustments (-999999)".into()); @@ -880,6 +886,30 @@ mod tests { assert_eq!(partial["light"]["exposure"], 0.75); } + #[test] + fn catalog_crops_without_has_crop_are_imported() { + // Lightroom Classic catalogs store crop edges and angle but never `HasCrop` + let (partial, unknown) = mapped_settings( + "s = { CropAngle = 2.39897, CropConstrainAspectRatio = true, CropLeft = 0.042702, CropRight = 0.957298, CropTop = 0.1 }", + true, + 1.5, + ) + .unwrap(); + let crop = &partial["crop"]["geometry"]; + assert_eq!(crop["rect"]["x0"], 0.042702); + assert_eq!(crop["rect"]["y0"], 0.1); + assert_eq!(crop["rect"]["x1"], 0.957298); + assert_eq!(crop["rect"]["y1"], 1.0); + assert_eq!(crop["angle"], 2.39897); + assert!(!unknown.iter().any(|k| k.starts_with("CropLeft") || k.starts_with("CropAngle")), "{unknown:?}"); + // an angle alone is a straightened, uncropped frame + let (partial, _) = mapped_settings("s = { CropAngle = -1.5 }", true, 1.5).unwrap(); + assert_eq!(partial["crop"]["geometry"]["angle"], -1.5); + // no crop fields: the crop stays untouched + let (partial, _) = mapped_settings("s = { Exposure2012 = 0.5 }", true, 1.5).unwrap(); + assert!(partial.get("crop").is_none()); + } + #[test] fn deferred_auto_tone_sentinels_never_become_real_slider_values() { let mut data = sample();