mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Actions: export Box Blur, Surface Blur, Flatten Image and Black & White under ids Photoshop runs (#6330) (#6331)
Save Actions wrote these four steps under the four-char codes BoxB, SrfB, Fltn and BlnW, which Photoshop does not run: executeAction fails with "the command is not currently available" (measured in Photoshop on a document with a filled pixel layer; Fltn resolves to the flatness property, not the flatten event). The names boxblur, surfaceBlur and blackAndWhite and the code FltI run. - Box Blur, Surface Blur and Black & White now export by name (boxblur, surfaceBlur, blackAndWhite); Flatten Image exports as FltI. The descriptors are unchanged. - Import accepts the names, FltI and flattenImage, and still accepts the old codes (a LEGACY_CODES table consulted only for four-char codes), so actions PhotoCraft saved before keep loading. - Tests: each step exports the new id and imports back to the same command and params under the new id, the name and the legacy code; a set mixing Photoshop-shaped and legacy steps loads and re-saves with the new ids; near-miss ids (case, padding, empty, NULs) stay Photoshop steps; broken descriptors don't panic. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
7200f64bf1
commit
dfe94d6ab9
@@ -2691,8 +2691,8 @@ const EVENTS: &[Event] = &[
|
||||
ev!("DstS", "dustAndScratches", "Dust & Scratches", "filter.noise.dustAndScratches", &[F("Rds ", "radius", Kind::Px), F("Thsh", "threshold", Kind::Long)]),
|
||||
ev!("Dspc", "despeckle", "Despeckle", "filter.noise.despeckle", &[]),
|
||||
ev!("MtnB", "motionBlur", "Motion Blur", "filter.blur.motionBlur", &[F("Angl", "angle", Kind::Ang), F("Dstn", "distance", Kind::Px)]),
|
||||
ev!("BoxB", "boxblur", "Box Blur", "filter.blur.boxBlur", &[F("Rds ", "radius", Kind::Px)]),
|
||||
ev!("SrfB", "surfaceBlur", "Surface Blur", "filter.blur.surfaceBlur", &[F("Rds ", "radius", Kind::Px), F("Thsh", "threshold", Kind::Long)]),
|
||||
ev!("", "boxblur", "Box Blur", "filter.blur.boxBlur", &[F("Rds ", "radius", Kind::Px)]),
|
||||
ev!("", "surfaceBlur", "Surface Blur", "filter.blur.surfaceBlur", &[F("Rds ", "radius", Kind::Px), F("Thsh", "threshold", Kind::Long)]),
|
||||
ev!(
|
||||
"SmrB",
|
||||
"smartBlur",
|
||||
@@ -2768,7 +2768,7 @@ const EVENTS: &[Event] = &[
|
||||
adjustment_event!("Exps", "Exps", "exposure", "Exposure"),
|
||||
adjustment_event!("PhtF", "PhtF", "photoFilter", "Photo Filter"),
|
||||
Event {
|
||||
code: "BlnW",
|
||||
code: "",
|
||||
name: "blackAndWhite",
|
||||
label: "Black & White",
|
||||
ids: &["image.adjustments.blackWhite"],
|
||||
@@ -2960,7 +2960,7 @@ const EVENTS: &[Event] = &[
|
||||
from: move_from,
|
||||
to: move_to,
|
||||
},
|
||||
ev!("Fltn", "flattenImage", "Flatten Image", "layer.flattenImage", &[]),
|
||||
ev!("FltI", "flattenImage", "Flatten Image", "layer.flattenImage", &[]),
|
||||
Event {
|
||||
code: "MrgV",
|
||||
name: "mergeVisible",
|
||||
@@ -3046,9 +3046,20 @@ const EVENTS: &[Event] = &[
|
||||
Event { code: "Stop", name: "stop", label: "Stop", ids: &[STOP_MESSAGE], from: stop_from, to: stop_to },
|
||||
];
|
||||
|
||||
/// Four-char codes PhotoCraft once exported for events Photoshop only runs by name (or by
|
||||
/// another code), with the name they stand for. Measured in Photoshop: `executeAction` with
|
||||
/// these codes fails ("the command is not currently available"), and `Fltn` is the
|
||||
/// `flatness` property, not the flatten event. Import keeps accepting them so actions saved
|
||||
/// before still load; export never writes them.
|
||||
const LEGACY_CODES: &[(&str, &str)] = &[("BoxB", "boxblur"), ("SrfB", "surfaceBlur"), ("Fltn", "flattenImage"), ("BlnW", "blackAndWhite")];
|
||||
|
||||
fn event_for(item: &AtnEvent) -> Option<&'static Event> {
|
||||
let id = item.id();
|
||||
EVENTS.iter().find(|e| (!e.code.is_empty() && e.code.trim_end() == id.trim_end()) || e.name == id)
|
||||
let find = |id: &str| EVENTS.iter().find(|e| (!e.code.is_empty() && e.code.trim_end() == id.trim_end()) || e.name == id);
|
||||
find(&id).or_else(|| match item {
|
||||
AtnEvent::Code(_) => LEGACY_CODES.iter().find(|(code, _)| *code == id).and_then(|(_, name)| find(name)),
|
||||
AtnEvent::Name(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The event `id` exports as: the first event listing it whose converter accepts the params
|
||||
|
||||
@@ -144,7 +144,7 @@ fn save_round_trips_through_photoshop_events() {
|
||||
assert_eq!(file.name.to_string_lossy(), "Test Set");
|
||||
assert_eq!(file.actions[0].function_key, 3);
|
||||
let events: Vec<String> = file.actions[0].items.iter().map(|i| i.event.id()).collect();
|
||||
assert_eq!(events, ["GsnB", "Mk ", "setd", "slct", "slct", "Stop", "syntheticFill", "Ply ", "Fltn"]);
|
||||
assert_eq!(events, ["GsnB", "Mk ", "setd", "slct", "slct", "Stop", "syntheticFill", "Ply ", "FltI"]);
|
||||
assert!(file.actions[0].items[0].with_dialog);
|
||||
assert!(!file.actions[0].items[8].enabled);
|
||||
// The kept Photoshop step is written back byte for byte.
|
||||
@@ -1037,6 +1037,77 @@ fn remove_white_clears_the_targeted_layer_not_the_converted_background() {
|
||||
assert!(bottom.surface().unwrap().sample_channel(2, 2, 1) > 0.99, "and kept its pixels");
|
||||
}
|
||||
|
||||
/// Measured in Photoshop: `executeAction` fails for the codes `BoxB`, `SrfB`, `Fltn` and
|
||||
/// `BlnW` ("the command is not currently available"), while the names `boxblur`,
|
||||
/// `surfaceBlur`, `flattenImage`, `blackAndWhite` and the code `FltI` run.
|
||||
#[test]
|
||||
fn events_photoshop_only_runs_by_name_export_by_name_and_old_codes_still_import() {
|
||||
let bw = json!({"reds": 40, "yellows": 60, "greens": 40, "cyans": 60, "blues": 20, "magentas": 80, "tint": false, "tintColor": "#e4d5b0"});
|
||||
let cases = [
|
||||
("filter.blur.boxBlur", json!({"radius": 3}), "boxblur", "BoxB"),
|
||||
("filter.blur.surfaceBlur", json!({"radius": 5, "threshold": 15}), "surfaceBlur", "SrfB"),
|
||||
("layer.flattenImage", json!({}), "FltI", "Fltn"),
|
||||
("image.adjustments.blackWhite", bw, "blackAndWhite", "BlnW"),
|
||||
];
|
||||
for (id, params, written, legacy) in &cases {
|
||||
let exported = export_item(id, params, "S").unwrap_or_else(|| panic!("{id} does not export"));
|
||||
assert_eq!(exported.event.id(), *written, "{id} exports under an id Photoshop runs");
|
||||
assert_eq!(matches!(exported.event, AtnEvent::Name(_)), written.len() != 4, "{id}: a name is written as a name");
|
||||
let d = exported.descriptor.clone();
|
||||
// The written id, the name for Flatten Image, and the old code all import to the command.
|
||||
let name = if *id == "layer.flattenImage" { "flattenImage" } else { written };
|
||||
for event in [*written, name, *legacy] {
|
||||
let e = event_for(&item(event, "", None).event).unwrap_or_else(|| panic!("{event} does not import"));
|
||||
assert!(e.ids.contains(id), "{event} imports as {:?}, not {id}", e.ids);
|
||||
match (e.from)(d.as_ref().unwrap()) {
|
||||
Mapped::Steps(steps) => {
|
||||
assert_eq!(steps.len(), 1, "{event}: {steps:?}");
|
||||
assert_eq!(steps[0].0, *id, "{event}");
|
||||
let mut back = steps[0].1.clone();
|
||||
normalise(&mut back);
|
||||
assert_eq!(back, *params, "{event}");
|
||||
}
|
||||
Mapped::Unsupported { .. } => panic!("{event} does not import back"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A whole set of Photoshop-shaped steps (names, FltI) and old PhotoCraft-shaped ones
|
||||
// (legacy codes) loads to the same commands.
|
||||
let blur = |event: &str| item(event, "Box Blur", Some(desc("null").with("Rds ", px(3.0))));
|
||||
let surface = |event: &str| item(event, "Surface Blur", Some(desc("null").with("Rds ", px(5.0)).with("Thsh", long(15.0))));
|
||||
let photoshop = vec![blur("boxblur"), surface("surfaceBlur"), item("FltI", "Flatten Image", None), item("flattenImage", "Flatten Image", None)];
|
||||
let legacy = vec![blur("BoxB"), surface("SrfB"), item("Fltn", "Flatten Image", None)];
|
||||
let data = set_of("S", vec![("Photoshop", photoshop), ("Legacy", legacy)]);
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("actions.load", json!({"data": data})).unwrap();
|
||||
assert_eq!(r["unsupported"], json!([]), "{r}");
|
||||
let ids = |i: usize| s.actions.list[i].steps.iter().map(|(id, _)| id.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(ids(0), ["filter.blur.boxBlur", "filter.blur.surfaceBlur", "layer.flattenImage", "layer.flattenImage"]);
|
||||
assert_eq!(ids(1), ["filter.blur.boxBlur", "filter.blur.surfaceBlur", "layer.flattenImage"]);
|
||||
assert_eq!(s.actions.list[1].steps[1].1, json!({"radius": 5, "threshold": 15}));
|
||||
// Saving them writes the ids Photoshop runs, never the old codes.
|
||||
let saved = s.execute("actions.save", json!({"set": "S"})).unwrap();
|
||||
let file = atn::parse(&photocraft_paint::tile::b64_decode(saved["data"].as_str().unwrap()).unwrap()).unwrap();
|
||||
let events: Vec<String> = file.actions.iter().flat_map(|a| a.items.iter().map(|i| i.event.id())).collect();
|
||||
assert_eq!(events, ["boxblur", "surfaceBlur", "FltI", "FltI", "boxblur", "surfaceBlur", "FltI"]);
|
||||
|
||||
// Unknown ids, near misses and the legacy codes written as names stay Photoshop steps,
|
||||
// never a panic or a wrong command.
|
||||
for event in ["BoxBlur", "boxBlur", "BOXB", "Fltn ", "FLTI", "", "\u{0}\u{0}\u{0}\u{0}", "BlnWx"] {
|
||||
let e = event_for(&item(event, "", None).event);
|
||||
assert!(e.is_none(), "{event:?} maps to {:?}", e.map(|e| e.ids));
|
||||
}
|
||||
// A legacy code with a broken descriptor is unsupported, not a crash.
|
||||
let e = event_for(&item("SrfB", "", None).event).unwrap();
|
||||
assert!(matches!((e.from)(&desc("null").with("Rds ", D::Double(f64::NAN))), Mapped::Unsupported { .. } | Mapped::Steps(_)));
|
||||
// A wrong-typed key is skipped like any other unreadable field: the step keeps the defaults.
|
||||
match (e.from)(&desc("null").with("Rds ", text("x"))) {
|
||||
Mapped::Steps(steps) => assert!(steps.iter().all(|(_, p)| p.get("radius").is_none()), "{steps:?}"),
|
||||
Mapped::Unsupported { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Smart Blur's quality uses Photoshop's `SmBQ` codes (`SBQL`/`SBQM`/`SBQH`): measured in
|
||||
/// Photoshop, the old `Hgh `/`Mdm `/`Low ` play back as its fallback quality. Old codes still read.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user