mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 18:13:35 +00:00
Integration: Custom import takes whole numbers only, export refuses non-object params (#10631)
A Custom (Cstm) step with a fractional Scl/Ofst/cell, e.g. Scl Double 1.5, was mapped to filter.other.custom with scale 1.5, but export writes integers and refuses the fraction, so Save Actions failed on a step that used to stay a kept Photoshop step and round-trip. `within` now requires a whole number; an integral Double such as 2.0 still maps. `sets_only` accepted any non-object params, so custom_to(json!([1])) wrote an identity Custom and bare_to(json!(5)) an empty descriptor for params no command can play. Only an object or null passes now. Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
hectorviov
Claude Opus 5.5
parent
4ed3f5b591
commit
632a62b554
@@ -23,9 +23,10 @@ fn setting<T>(d: &Descriptor, key: &str, read: impl Fn(&D) -> Option<T>, default
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `p` sets nothing but `allowed` (a `null` counts as unset).
|
||||
/// Whether `p` sets nothing but `allowed` (a `null` counts as unset). Params that are neither an
|
||||
/// object nor `null` are no command's params, so they set something no event can hold.
|
||||
fn sets_only(p: &Value, allowed: &[&str]) -> bool {
|
||||
p.as_object().is_none_or(|m| m.iter().all(|(k, v)| v.is_null() || allowed.contains(&k.as_str())))
|
||||
p.is_null() || p.as_object().is_some_and(|m| m.iter().all(|(k, v)| v.is_null() || allowed.contains(&k.as_str())))
|
||||
}
|
||||
|
||||
/// NTSC Colors, Average and Remove Black / White Matte: Photoshop runs them with an empty
|
||||
@@ -91,15 +92,15 @@ const CUSTOM_SCALE: RangeInclusive<f64> = 1.0..=9999.0;
|
||||
const CUSTOM_OFFSET: RangeInclusive<f64> = -9999.0..=9999.0;
|
||||
const CUSTOM_CELL: RangeInclusive<f64> = -999.0..=999.0;
|
||||
|
||||
/// A number within `range`.
|
||||
/// A whole number within `range`.
|
||||
fn within(range: &RangeInclusive<f64>) -> impl Fn(&D) -> Option<f64> + '_ {
|
||||
move |v| num(v).filter(|n| range.contains(n))
|
||||
move |v| num(v).filter(|n| n.fract() == 0.0 && range.contains(n))
|
||||
}
|
||||
|
||||
/// Custom (`Cstm`): `Mtrx` is the 5 × 5 kernel, row by row. Photoshop takes exactly 25 cells (a
|
||||
/// 3 × 3 list makes it fail), so any other matrix keeps the Photoshop step, as does a `Scl `,
|
||||
/// `Ofst` or cell that is not a number, or one outside the command's range (it would play
|
||||
/// clamped).
|
||||
/// `Ofst` or cell that is not a number, one that is not a whole number (export writes integers,
|
||||
/// so Save Actions would refuse it), or one outside the command's range (it would play clamped).
|
||||
pub(super) fn custom_from(d: &Descriptor) -> Mapped {
|
||||
let unsupported = Mapped::Unsupported { harmless: false };
|
||||
if !only_keys(d, &["Scl", "Ofst", "Mtrx"]) {
|
||||
|
||||
@@ -3297,9 +3297,14 @@ fn video_other_and_matting_steps_that_cannot_be_written_are_refused() {
|
||||
("filter.blur.average", json!({"target": "mask"})),
|
||||
("filter.other.hsbHsl", json!({"inputMode": "lab"})),
|
||||
("filter.video.deInterlace", json!({"eliminate": "allFields"})),
|
||||
("filter.other.custom", json!([1])),
|
||||
("filter.blur.average", json!(5)),
|
||||
("filter.other.hsbHsl", json!("hsl")),
|
||||
] {
|
||||
assert!(export_item(id, ¶ms, "S").is_none(), "{id} {params} exported");
|
||||
}
|
||||
// `null` params set nothing.
|
||||
assert!(export_item("filter.blur.average", &Value::Null, "S").is_some());
|
||||
// A Custom step without a kernel is the command's identity kernel.
|
||||
let d = export_item("filter.other.custom", &json!({}), "S").unwrap().descriptor.unwrap();
|
||||
let cells = get_list(&d, "Mtrx").unwrap();
|
||||
@@ -3352,6 +3357,10 @@ fn custom_outside_the_commands_range_is_kept_and_refused() {
|
||||
desc("null").with("Mtrx", kernel(-1000)),
|
||||
desc("null").with("Mtrx", D::List((0..26).map(|_| D::Integer(0)).collect())),
|
||||
desc("null").with("Mtrx", D::List((0..1000).map(|_| D::Integer(0)).collect())),
|
||||
// Fractions: export writes integers, so Save Actions would refuse the mapped step.
|
||||
desc("null").with("Mtrx", identity.clone()).with("Scl ", D::Double(1.5)),
|
||||
desc("null").with("Mtrx", identity.clone()).with("Ofst", D::Double(2.5)),
|
||||
desc("null").with("Mtrx", D::List((0..25).map(|i| if i == 12 { D::Double(0.5) } else { D::Integer(0) }).collect())),
|
||||
] {
|
||||
assert!(kept(other_step("Cstm", d.clone())), "{:?} was mapped", d.items);
|
||||
}
|
||||
@@ -3363,6 +3372,9 @@ fn custom_outside_the_commands_range_is_kept_and_refused() {
|
||||
(&json!(f64::from(scale)), &json!(f64::from(offset)), &json!(f64::from(cell)))
|
||||
);
|
||||
}
|
||||
// An integral Double is a whole number and still maps.
|
||||
let Mapped::Steps(steps) = other_step("Cstm", desc("null").with("Mtrx", identity.clone()).with("Scl ", D::Double(2.0))) else { panic!("Scl 2.0 kept") };
|
||||
assert_eq!(steps[0].1["scale"], json!(2.0));
|
||||
let mut cells = vec![json!(0); 25];
|
||||
cells[12] = json!(1000);
|
||||
for params in [json!({"scale": 0}), json!({"scale": 10_000}), json!({"offset": -10_000}), json!({"offset": 10_000}), json!({"kernel": cells})] {
|
||||
|
||||
Reference in New Issue
Block a user