Keep arbitrary canvas rotation interpolation in ATN export (#7630)

Fixes #7504

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-19 13:31:49 -04:00
committed by GitHub
co-authored by Claude Haiku 5.5
parent cbe6c4e131
commit fe64e5a2bd
2 changed files with 38 additions and 2 deletions
+19 -2
View File
@@ -2135,7 +2135,11 @@ fn rotate_from(d: &Descriptor) -> Mapped {
} else if targets_layer(d) {
return Mapped::Unsupported { harmless: false };
} else {
return step("image.rotation.arbitrary", json!({"angle": angle.abs(), "direction": if angle < 0.0 { "ccw" } else { "cw" }}));
let mut p = json!({"angle": angle.abs(), "direction": if angle < 0.0 { "ccw" } else { "cw" }});
if let Some(i) = get_enum(d, "Intr").and_then(|i| lookup(INTERPOLATION, &i)).filter(|i| matches!(*i, "bicubic" | "bilinear" | "nearest")) {
p["interpolation"] = i.into();
}
return step("image.rotation.arbitrary", p);
};
none(if targets_layer(d) { layer } else { doc })
}
@@ -2155,7 +2159,20 @@ fn rotate_to(id: &str, p: &Value) -> Option<Descriptor> {
if layer {
d = d.with("null", target_ref("Lyr "));
}
Some(d.with("Angl", ang(angle)))
d = d.with("Angl", ang(angle));
if id == "image.rotation.arbitrary"
&& let Some(i) = p.get("interpolation")
{
// The command resamples with nearest, bilinear or bicubic; any other value can't be
// carried faithfully, so refuse rather than replay with a different interpolation.
let i = match i.as_str()? {
"nearestNeighbor" => "nearest",
i @ ("bicubic" | "bilinear" | "nearest") => i,
_ => return None,
};
d = d.with("Intr", enm("Intp", lookup_back(INTERPOLATION, i)?));
}
Some(d)
}
fn flip_from(d: &Descriptor) -> Mapped {
+19
View File
@@ -1199,6 +1199,25 @@ fn fractional_translation_only_transform_keeps_offset_and_interpolation() {
assert_eq!(steps[0].1.get("interpolation"), Some(&json!("bilinear")));
}
/// Arbitrary canvas rotation must keep its interpolation through a strict export: dropping it
/// silently replays the rotation with the default bicubic resampling.
#[test]
fn arbitrary_rotation_keeps_interpolation_through_atn() {
for params in [
json!({"angle": 17, "direction": "ccw", "interpolation": "nearest"}),
json!({"angle": 17, "direction": "ccw", "interpolation": "bilinear"}),
json!({"angle": 17, "direction": "ccw", "interpolation": "bicubic"}),
json!({"angle": 17, "direction": "ccw"}),
] {
let a = Action { name: "Rot".into(), steps: vec![("image.rotation.arbitrary".into(), params.clone())], ..Default::default() };
let (bytes, skipped) = export("Rot", &[&a], &[], false).unwrap();
assert!(skipped.is_empty());
let imported = import(&bytes).unwrap();
assert!(imported.unsupported.is_empty());
assert_eq!(imported.actions[0].steps, vec![("image.rotation.arbitrary".to_string(), params)]);
}
}
/// A filter recorded against a layer mask must not export as a plain filter that edits the
/// layer's pixels on replay: Photoshop's descriptor has no field for PhotoCraft's `target`.
#[test]