From 8574f8d80983cdacd8d6e27cc5ef6374ebe0ad68 Mon Sep 17 00:00:00 2001 From: David Mann Date: Sat, 17 Oct 2026 16:39:44 +0200 Subject: [PATCH] Merge to HDR Pro and merged duplicates: the Background layer is a real Background (#7078) Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/image_cmds.rs | 24 +++++++++++++++++++++++- crates/engine/src/photo_cmds.rs | 7 ++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/image_cmds.rs b/crates/engine/src/image_cmds.rs index 3e870aa686..cfdc373795 100644 --- a/crates/engine/src/image_cmds.rs +++ b/crates/engine/src/image_cmds.rs @@ -735,7 +735,13 @@ fn duplicate(s: &mut Session, p: &Value) -> Result { let vals: Vec = buf.px.iter().flat_map(|q| photocraft_raster::from_rgba(&fmt, *q)).collect(); surf.write_region(doc.bounds(), &vals); surf.prune(); - doc.layers = vec![Layer::new("Background", LayerContent::Raster(surf))]; + // As Photoshop: a source with a Background gives one (opaque, locked); otherwise a plain + // "Layer 1" that keeps the composite's transparency. + let has_background = doc.background().is_some(); + let mut merged = Layer::new(if has_background { "Background" } else { "Layer 1" }, LayerContent::Raster(surf)); + merged.locks.transparency = has_background; + merged.locks.position = has_background; + doc.layers = vec![merged]; // A merged-only copy has no original layers for saved comps to restore. // Don't carry dangling comp state into a single-layer flattened document. doc.layer_comps.clear(); @@ -1490,6 +1496,22 @@ mod tests { assert_eq!(doc(&s).depth, SampleType::U8); } + /// Duplicate › Merged Layers Only gives one locked Background when the source has one and a + /// plain "Layer 1" (keeping transparency) when it doesn't, as Photoshop does. + #[test] + fn merged_duplicate_is_a_background_only_when_the_source_has_one() { + let mut s = session(); + s.execute("image.duplicate", json!({"mergedOnly": true})).unwrap(); + let d = doc(&s); + assert!(d.is_background(d.layers[0].id), "source has a Background: the merged copy is one"); + let mut s = Session::new(); + s.execute("file.new", json!({"width": 8, "height": 8, "background": "transparent"})).unwrap(); + s.execute("image.duplicate", json!({"mergedOnly": true})).unwrap(); + let d = doc(&s); + assert!(d.background().is_none(), "transparent source: no Background in the copy"); + assert_eq!(d.layers[0].name, "Layer 1"); + } + #[test] fn duplicate_document() { let mut s = session(); diff --git a/crates/engine/src/photo_cmds.rs b/crates/engine/src/photo_cmds.rs index 09fd474aad..d4c70504f8 100644 --- a/crates/engine/src/photo_cmds.rs +++ b/crates/engine/src/photo_cmds.rs @@ -799,7 +799,11 @@ fn merge_to_hdr(s: &mut Session, p: &Value) -> Result { } let mut surf = Surface::new(fmt); surf.write_region(area, &data); - doc.layers.push(Layer::new("Background", LayerContent::Raster(surf))); + // The merged result is opaque: a real Background, as Crop and Straighten makes. + let mut bg = Layer::new("Background", LayerContent::Raster(surf)); + bg.locks.transparency = true; + bg.locks.position = true; + doc.layers.push(bg); let idx = s.add_document(doc, None); let ev0 = exposures[order[0]]; Ok(json!({ @@ -1080,6 +1084,7 @@ mod tests { assert!((ev[1] - 2.0).abs() < 1e-6 && (ev[2] - 4.0).abs() < 1e-6, "{ev:?}"); let d = &s.active().unwrap().doc; assert_eq!(d.depth, SampleType::F32); + assert!(d.is_background(d.layers[0].id), "the merged layer is a real (locked) Background"); let l = d.layers[0].surface().unwrap(); // Linear output: ratios across the ramp follow the radiance. let (a, b) = (l.rgba(20, 30)[1] as f64, l.rgba(80, 30)[1] as f64);