From 307ec1a053dea397537d41e110d66c2ddfea539c Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Sun, 18 Oct 2026 17:41:38 +0200 Subject: [PATCH] Lightroom catalog import: size radial masks with the stored image's aspect (part of #1806) --- crates/engine/src/lightroom_catalog.rs | 12 ++++++++---- crates/engine/src/lightroom_catalog/tests.rs | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/lightroom_catalog.rs b/crates/engine/src/lightroom_catalog.rs index 4cc6c6c1e..12934c0e0 100644 --- a/crates/engine/src/lightroom_catalog.rs +++ b/crates/engine/src/lightroom_catalog.rs @@ -703,7 +703,11 @@ fn mapped_settings( if !props.contains_key("crs:HasCrop") && CROP.iter().any(|k| props.contains_key(*k)) { props.insert("crs:HasCrop".into(), vec!["True".into()]); } - let (mut partial, mut unknown) = crate::crs::to_partial_report(&props, Some(&values), target, aspect); + // Mask edges are fractions of the stored image, like the crop's, so radii go to long-edge units + // with its aspect: `aspect` is the photo's in LightCraft, which is the shown one for a photo + // already in the library and only stands in when the catalog has no file size. + let mask_aspect = if stored_aspect.is_finite() && stored_aspect > 0.0 { stored_aspect } else { aspect }; + let (mut partial, mut unknown) = crate::crs::to_partial_report(&props, Some(&values), target, mask_aspect); catalog_crop(&mut partial, orientation, stored_aspect); catalog_masks(&mut partial, orientation); if deferred > 0 { @@ -767,9 +771,9 @@ fn catalog_crop(partial: &mut Value, orientation: Orientation, stored_aspect: f6 /// Lightroom's masks in LightCraft's frame. Like the crop's corners, their positions are on the /// stored image (before orientation), normalized to its size; LightCraft's are on the oriented -/// image. So the points go through the orientation, a radial's radii (in long-edge units) swap when -/// the axes do, and a mirror turns its angle the other way. A quarter turn keeps the angle: the -/// ellipse turned 90° is the same ellipse with its radii swapped. +/// image. So the points go through the orientation, a radial's radii (in long-edge units, from the +/// stored image's aspect) swap when the axes do, and a mirror turns its angle the other way. A +/// quarter turn keeps the angle: the ellipse turned 90° is the same ellipse with its radii swapped. fn catalog_masks(partial: &mut Value, orientation: Orientation) { if orientation == Orientation::Normal { return; diff --git a/crates/engine/src/lightroom_catalog/tests.rs b/crates/engine/src/lightroom_catalog/tests.rs index e420a995e..857de2f5c 100644 --- a/crates/engine/src/lightroom_catalog/tests.rs +++ b/crates/engine/src/lightroom_catalog/tests.rs @@ -332,6 +332,13 @@ fn catalog_masks_follow_the_photo_orientation() { assert_eq!(masks(Orientation::Rotate180), [0.9, 0.8, 0.7, 0.4, 0.9, 0.8, r.0, r.1, 10.0, 0.9, 0.8, 0.7, 0.4]); assert_eq!(masks(Orientation::FlipH), [0.9, 0.2, 0.7, 0.6, 0.9, 0.2, r.0, r.1, -10.0, 0.9, 0.2, 0.7, 0.6]); assert_eq!(masks(Orientation::Transpose), [0.2, 0.1, 0.6, 0.3, 0.2, 0.1, r.1, r.0, -10.0, 0.2, 0.1, 0.6, 0.3]); + // a photo already in the library has its shown size (portrait here), but Lightroom's edges are + // fractions of the stored image: a circle on a 3:2 stored image stays a circle + let s = r#"s = { MaskGroupBasedCorrections = { { What = "Correction", CorrectionAmount = 1, LocalExposure2012 = 0.1, CorrectionMasks = { + { What = "Mask/CircularGradient", Left = 0.3, Right = 0.5, Top = 0.3, Bottom = 0.6, Version = 2, Flipped = true } } } } }"#; + let (partial, _) = mapped_settings(s, crate::crs::Target::RawAbsolute, 1.0 / 1.5, Orientation::Rotate90, 1.5).unwrap(); + let radial = &partial["masks"][0]["components"][0]["shape"]; + assert!(close(&radial["rx"], 0.1) && close(&radial["ry"], 0.1), "{radial}"); } /// The `crs` corners and angle Lightroom would store for LightCraft crop `g` on a stored image