fix(atn): keep Photoshop's other transform interpolation modes on import (#8698)

The .atn importer kept only bicubic, bilinear and nearest from a
transform's Intr, so a recorded Bicubic Automatic, Sharper or Smoother
Transform Selection replayed with the command's default (bilinear) and
re-exported without its choice. The bicubic variants are now kept (the
transforms already render them as Bicubic), Automatic becomes Bicubic
Automatic and Preserve Details Bicubic, and both transform commands list
the variants (#8697).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
FinnNk
2026-10-23 17:55:54 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 2b83be6c81
commit fc0b40dae5
4 changed files with 53 additions and 3 deletions
+9 -1
View File
@@ -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 {
+22
View File
@@ -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.
+21 -1
View File
@@ -250,7 +250,7 @@ pub fn specs() -> Vec<CommandSpec> {
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::<Vec<_>>())
};
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] {
+1 -1
View File
@@ -859,7 +859,7 @@ pub fn specs() -> Vec<CommandSpec> {
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,