diff --git a/crates/engine/src/crs.rs b/crates/engine/src/crs.rs index 0b0c83b9..11962333 100644 --- a/crates/engine/src/crs.rs +++ b/crates/engine/src/crs.rs @@ -244,13 +244,19 @@ pub fn to_partial(props: &Props, raw: Option) -> Value { put(o, "curve.master", c); } } - // Lightroom Classic reads the red / green / blue curves only together with the master 2012 - // curve: a channel curve without `ToneCurvePV2012` leaves the photo unchanged + // Lightroom Classic reads the red / green / blue curves only when the packet holds the master + // 2012 curve and all three channel curves (as its own packets always do): a packet missing any + // of them imports without channel curves (master + red, master + red + green, or red / green / + // blue without the master all leave the photo's channels unchanged there) if let Some(c) = curve(props, "crs:ToneCurvePV2012") { put(o, "curve.master", c); - for (crs, ch) in [("ToneCurvePV2012Red", "red"), ("ToneCurvePV2012Green", "green"), ("ToneCurvePV2012Blue", "blue")] { - if let Some(c) = curve(props, &format!("crs:{crs}")) { - put(o, &format!("curve.{ch}"), c); + let channels = [("ToneCurvePV2012Red", "red"), ("ToneCurvePV2012Green", "green"), ("ToneCurvePV2012Blue", "blue")] + .map(|(crs, ch)| (curve(props, &format!("crs:{crs}")), ch)); + if channels.iter().all(|(c, _)| c.is_some()) { + for (c, ch) in channels { + if let Some(c) = c { + put(o, &format!("curve.{ch}"), c); + } } } } @@ -584,21 +590,34 @@ mod tests { } #[test] - fn channel_curves_need_the_master_curve() { - // Lightroom ignores a red / green / blue curve without `ToneCurvePV2012` - let packet = |master: &str| { + fn channel_curves_need_the_master_and_all_channel_curves() { + // Lightroom applies red / green / blue curves only when the master and all three are present + let seq = |name: &str, pts: &str| format!("{pts}"); + let identity = "0, 0255, 255"; + let bent = "0, 064, 40255, 255"; + let packet = |curves: &[String]| { format!( r#" - {master} - 0, 064, 40255, 255 - "# + {} + "#, + curves.concat() ) }; - let alone = to_partial(&props(&packet("")), Some(true)); + let partial = |curves: &[String]| to_partial(&props(&packet(curves)), Some(true)); + let (master, red, green, blue) = ( + seq("ToneCurvePV2012", identity), + seq("ToneCurvePV2012Red", identity), + seq("ToneCurvePV2012Green", bent), + seq("ToneCurvePV2012Blue", identity), + ); + // a channel curve alone, or with the master but without its two siblings: ignored + let alone = partial(std::slice::from_ref(&green)); assert!(alone.pointer("/curve/green").is_none(), "{alone}"); - let master = "0, 0255, 255"; - let with = to_partial(&props(&packet(master)), Some(true)); - assert!(with.pointer("/curve/green").is_some() && with.pointer("/curve/master").is_some(), "{with}"); + let master_and_green = partial(&[master.clone(), green.clone()]); + assert!(master_and_green.pointer("/curve/green").is_none() && master_and_green.pointer("/curve/master").is_some(), "{master_and_green}"); + // all four: read + let all = partial(&[master, red, green, blue]); + assert!(["/curve/master", "/curve/red", "/curve/green", "/curve/blue"].iter().all(|p| all.pointer(p).is_some()), "{all}"); } fn unmapped(x: &str) -> Vec { diff --git a/docs/whats-new.md b/docs/whats-new.md index ccb0be80..821ecc16 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -81,7 +81,8 @@ white, empty Point Color slots…) as settings that couldn't be carried over. - A preset whose lens-profile switch is off no longer turns off the lens corrections built into a DNG (iPhone ProRAW and other files with embedded distortion / vignetting corrections), matching what the preset does elsewhere. -- A red / green / blue curve in an XMP preset without the master curve is ignored, as Lightroom ignores it. +- XMP presets apply their red / green / blue curves only when they also have the master curve and all three channel + curves, as Lightroom does: a preset with just one channel curve (or no master curve) leaves the channels alone. - Imported `.cube` LUT profiles appear in the Profile menu and the profile browser, grouped by their folder, and stay favourites across restarts (issue #328). diff --git a/docs/xmp-interop.md b/docs/xmp-interop.md index 73543231..c7de1003 100644 --- a/docs/xmp-interop.md +++ b/docs/xmp-interop.md @@ -117,7 +117,7 @@ names are read (e.g. `Exposure2012`, not the older `Exposure`). | `ParametricShadows`, `ParametricDarks`, `ParametricLights`, `ParametricHighlights` | `curve.shadows/darks/lights/highlights` | | | `ParametricShadowSplit`, `ParametricMidtoneSplit`, `ParametricHighlightSplit` | `curve.split_shadows/split_mid/split_highlights` | | | `CurveRefineSaturation` | `curve.refine_saturation` | 0..100 (100 = curve saturation unchanged) | -| `ToneCurvePV2012`, `ToneCurvePV2012Red/Green/Blue` | `curve.master/red/green/blue` | `"x, y"` points in 0..255 → 0..1; a straight 0→255 line = no curve; the red / green / blue curves are read only with `ToneCurvePV2012` (Lightroom Classic ignores them without it) | +| `ToneCurvePV2012`, `ToneCurvePV2012Red/Green/Blue` | `curve.master/red/green/blue` | `"x, y"` points in 0..255 → 0..1; a straight 0→255 line = no curve; the red / green / blue curves are read only when `ToneCurvePV2012` and all three channel curves are present (Lightroom Classic ignores them otherwise: a master + red-only packet renders without the red curve) | | `SplitToningShadowHue/Saturation`, `SplitToningHighlightHue/Saturation` | `grading.shadows/highlights.hue/sat` | | | `ColorGradeShadowLum`, `ColorGradeHighlightLum` | `grading.shadows/highlights.lum` | | | `ColorGradeMidtoneHue/Sat/Lum`, `ColorGradeGlobalHue/Sat/Lum` | `grading.midtones/global.*` | |