diff --git a/crates/engine/src/atn_cmds.rs b/crates/engine/src/atn_cmds.rs index 30a05192ec..cedfa5aeed 100644 --- a/crates/engine/src/atn_cmds.rs +++ b/crates/engine/src/atn_cmds.rs @@ -1949,7 +1949,15 @@ fn transform_from(d: &Descriptor) -> Mapped { let whole_pixels = ["dx", "dy"].iter().all(|k| p.get(*k).and_then(Value::as_f64).is_none_or(|v| v.fract() == 0.0)); let geometry_only = p.keys().all(|k| k == "dx" || k == "dy") && whole_pixels; // The resampling applies to a selection as much as to a layer (#7515). - let interpolation = get_enum(d, "Intr").and_then(|i| lookup(INTERPOLATION, &i)).filter(|i| matches!(*i, "bicubic" | "bilinear" | "nearest")); + // Photoshop's bicubic variants are kept (the transforms render them as Bicubic, and they export + // back unchanged); Automatic and Preserve Details become the nearest bicubic choice. Dropping + // them fell back to the command's default, bilinear for a selection (#8697). + let interpolation = get_enum(d, "Intr").and_then(|i| lookup(INTERPOLATION, &i)).and_then(|i| match i { + "bicubic" | "bilinear" | "nearest" | "bicubicSharper" | "bicubicSmoother" | "bicubicAutomatic" => Some(i), + "automatic" => Some("bicubicAutomatic"), + "preserveDetails" => Some("bicubic"), + _ => None, + }); match (ref_class(r).as_deref(), target(r)) { (Some("Chnl"), Target::Property(prop)) if prop == "fsel" => { if let Some(i) = interpolation { diff --git a/crates/engine/src/atn_cmds/tests.rs b/crates/engine/src/atn_cmds/tests.rs index 02474b8858..cca5491979 100644 --- a/crates/engine/src/atn_cmds/tests.rs +++ b/crates/engine/src/atn_cmds/tests.rs @@ -2053,6 +2053,28 @@ fn options_the_descriptors_cannot_carry_refuse_strict_export() { } } +/// #8697: Photoshop's other transform interpolation modes survive import: its bicubic variants +/// are kept (they render as Bicubic and export back unchanged), Automatic becomes Bicubic +/// Automatic and Preserve Details Bicubic; before, they fell back to the command's default. +#[test] +fn photoshop_transform_interpolation_modes_survive_import() { + let one = |id: &str, p: Value| Action { name: "Probe".into(), steps: vec![(id.into(), p)], ..Default::default() }; + for id in ["select.transformSelection", "edit.transform"] { + for (mode, imported) in [ + ("bicubicSharper", "bicubicSharper"), + ("bicubicSmoother", "bicubicSmoother"), + ("bicubicAutomatic", "bicubicAutomatic"), + ("automatic", "bicubicAutomatic"), + ("preserveDetails", "bicubic"), + ] { + let (bytes, _) = export("Audit8", &[&one(id, json!({"scaleX": 140, "interpolation": mode}))], &[], false).unwrap(); + let steps = import(&bytes).unwrap().actions[0].steps.clone(); + let got = steps.first().and_then(|(_, p)| p.get("interpolation")).and_then(Value::as_str).map(str::to_owned); + assert_eq!(got.as_deref(), Some(imported), "{id} {mode}: {steps:?}"); + } + } +} + /// #8531: a partial `layer.setAdjustment` of Color Balance or Hue/Saturation can't be written /// (their descriptors carry every setting, filling absent ones with defaults), so strict export /// refuses it instead of replaying an edit that also resets the rest. Complete ones still export. diff --git a/crates/engine/src/select_extra_cmds.rs b/crates/engine/src/select_extra_cmds.rs index b3fae926eb..8ca012266d 100644 --- a/crates/engine/src/select_extra_cmds.rs +++ b/crates/engine/src/select_extra_cmds.rs @@ -250,7 +250,7 @@ pub fn specs() -> Vec { label: "Transform Selection", menu: &["Select"], shortcut: None, - params: r##"{"rect":[x0,y0,x1,y1]? (frame; default = selection bounds),"quad"|"corners":[[x,y]×4]? (where the frame's corners go: distort/perspective),"matrix":[a,b,c,d,e,f]?,"scaleX":%=100,"scaleY":%=100,"rotate":deg=0,"skewX":deg=0,"skewY":deg=0,"dx":px=0,"dy":px=0,"reference":"center|topLeft|top|topRight|left|right|bottomLeft|bottom|bottomRight"|[x,y]="center","style"|"mesh"|"grid"|"warp":… (warp, as edit.transform.warp),"interpolation":"bilinear|bicubic|nearest"="bilinear"}"##, + params: r##"{"rect":[x0,y0,x1,y1]? (frame; default = selection bounds),"quad"|"corners":[[x,y]×4]? (where the frame's corners go: distort/perspective),"matrix":[a,b,c,d,e,f]?,"scaleX":%=100,"scaleY":%=100,"rotate":deg=0,"skewX":deg=0,"skewY":deg=0,"dx":px=0,"dy":px=0,"reference":"center|topLeft|top|topRight|left|right|bottomLeft|bottom|bottomRight"|[x,y]="center","style"|"mesh"|"grid"|"warp":… (warp, as edit.transform.warp),"interpolation":"bilinear|bicubic|nearest|bicubicSharper|bicubicSmoother|bicubicAutomatic"="bilinear" (Photoshop's bicubic variants render as Bicubic)}"##, enabled: has_selection, run: transform_selection, journal: true, @@ -297,6 +297,26 @@ mod tests { s } + /// #8697: Photoshop's bicubic variants are accepted and render as Bicubic, for a selection + /// and for a layer (an imported action keeps them). + #[test] + fn photoshop_bicubic_variants_transform_as_bicubic() { + let run = |id: &str, interpolation: &str| { + let mut s = landscape(8); + s.execute("select.rect", json!({"x": 20, "y": 10, "width": 60, "height": 40, "feather": 3})).unwrap(); + s.execute(id, json!({"scaleX": 137, "scaleY": 81, "rotate": 7, "interpolation": interpolation})).unwrap(); + let st = s.active().unwrap(); + (st.doc.selection.clone(), st.doc.layers.iter().map(|l| l.surface().cloned()).collect::>()) + }; + for id in ["select.transformSelection", "edit.transform"] { + let bicubic = run(id, "bicubic"); + assert_ne!(bicubic, run(id, "bilinear"), "{id}: the interpolation matters here"); + for variant in ["bicubicSharper", "bicubicSmoother", "bicubicAutomatic"] { + assert_eq!(run(id, variant), bicubic, "{id} {variant}"); + } + } + } + #[test] fn sky_selects_the_sky_at_every_depth() { for depth in [8, 16, 32] { diff --git a/crates/engine/src/transform_cmds.rs b/crates/engine/src/transform_cmds.rs index fce8c1be49..a1b1aac379 100644 --- a/crates/engine/src/transform_cmds.rs +++ b/crates/engine/src/transform_cmds.rs @@ -859,7 +859,7 @@ pub fn specs() -> Vec { label: "Free Transform", menu: &[], shortcut: None, - params: r##"{"layer":id?,"rect":[x0,y0,x1,y1]? (source frame; default = layer content ∩ selection),"quad":[[x,y]×4]? (where the frame's corners go, clockwise from top-left),"matrix":[a,b,c,d,e,f]? (affine alternative),"scaleX":%?,"scaleY":%?,"rotate":deg?,"dx":px?,"dy":px? (relative alternative: scale and rotate about the frame's centre, then move, as a Photoshop action records a Free Transform),"interpolation":"bicubic|bilinear|nearest"="bicubic","target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target (an unlinked mask, an alpha channel or the Quick Mask transforms alone; a linked mask moves with its layer)}"##, + params: r##"{"layer":id?,"rect":[x0,y0,x1,y1]? (source frame; default = layer content ∩ selection),"quad":[[x,y]×4]? (where the frame's corners go, clockwise from top-left),"matrix":[a,b,c,d,e,f]? (affine alternative),"scaleX":%?,"scaleY":%?,"rotate":deg?,"dx":px?,"dy":px? (relative alternative: scale and rotate about the frame's centre, then move, as a Photoshop action records a Free Transform),"interpolation":"bicubic|bilinear|nearest|bicubicSharper|bicubicSmoother|bicubicAutomatic"="bicubic" (Photoshop's bicubic variants render as Bicubic),"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target (an unlinked mask, an alpha channel or the Quick Mask transforms alone; a linked mask moves with its layer)}"##, enabled: has_layer, journal: true, run: transform,