From 7bc6fd8a4f29a08eabbaed57fe3fa7a0d3a47813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Lu=C3=ADs=20da=20Silva?= <79342117+mercuryVM@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:40:11 -0300 Subject: [PATCH] Write the Levels and Curves composite channel as Chnl.Cmps in .atn export (#8686) Photoshop records the composite entry of a Levels or Curves Set as Chnl/Chnl/Cmps and silently leaves the adjustment unchanged when it is Chnl/Ordn/Trgt, which is what the export wrote. Import already reads both. Fixes #8543 --- crates/engine/src/atn_cmds.rs | 8 +++----- crates/engine/src/atn_cmds/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/atn_cmds.rs b/crates/engine/src/atn_cmds.rs index a3f7b0e0da..30a05192ec 100644 --- a/crates/engine/src/atn_cmds.rs +++ b/crates/engine/src/atn_cmds.rs @@ -377,13 +377,11 @@ fn channel_key(d: &Descriptor) -> Option> { }) } +/// A Levels or Curves entry's channel: `Chnl.Cmps` for the composite, as Photoshop records it. +/// Photoshop silently skips a Set whose composite is `Ordn.Trgt` (#8543). fn channel_ref(key: Option<&str>) -> D { let value = key.and_then(|k| lookup_back(CHANNELS, k)); - D::Reference(vec![ReferenceItem::Enumerated { - class: class("Chnl"), - type_id: code(if value.is_some() { "Chnl" } else { "Ordn" }), - value: code(value.unwrap_or("Trgt")), - }]) + D::Reference(vec![ReferenceItem::Enumerated { class: class("Chnl"), type_id: code("Chnl"), value: code(value.unwrap_or("Cmps")) }]) } // ------------------------------------------------------------------ field tables diff --git a/crates/engine/src/atn_cmds/tests.rs b/crates/engine/src/atn_cmds/tests.rs index 065080b02e..02474b8858 100644 --- a/crates/engine/src/atn_cmds/tests.rs +++ b/crates/engine/src/atn_cmds/tests.rs @@ -2383,6 +2383,32 @@ fn defaults_photoshop_plays_differently_are_written() { assert_eq!(get_list(&balance, "ShdL").map(<[D]>::len), Some(3)); } +/// Photoshop records the composite of a Levels or Curves entry as `Chnl.Cmps` and silently skips +/// a Set that names it `Ordn.Trgt` (#8543). A colour channel keeps its own code. +#[test] +fn levels_and_curves_name_the_composite_channel_as_photoshop_records_it() { + let exported = |id: &str, p: Value| export_item(id, &p, "S").and_then(|i| i.descriptor).unwrap_or_else(|| panic!("{id} does not export")); + let channels = |d: &Descriptor| -> Vec<(String, String)> { + get_list(d, "Adjs") + .unwrap_or_default() + .iter() + .filter_map(|e| match e { + D::Descriptor(e) => match get_ref(e, "Chnl")?.first()? { + ReferenceItem::Enumerated { type_id, value, .. } => Some((trimmed(type_id), trimmed(value))), + _ => None, + }, + _ => None, + }) + .collect() + }; + let composite = ("Chnl".to_string(), "Cmps".to_string()); + let red = ("Chnl".to_string(), "Rd".to_string()); + let levels = exported("image.adjustments.levels", json!({"inBlack": 31, "red": {"inBlack": 2}})); + assert_eq!(channels(&levels), vec![composite.clone(), red.clone()]); + let curves = exported("image.adjustments.curves", json!({"points": [[0, 0], [128, 140], [255, 255]], "red": [[0, 10], [255, 245]]})); + assert_eq!(channels(&curves), vec![composite, red]); +} + /// Photoshop sets one colour per step: a step that sets both doesn't export rather than drop one. #[test] fn set_colors_exports_only_a_single_colour() {