diff --git a/crates/engine/src/cmd/xform.rs b/crates/engine/src/cmd/xform.rs index 6bd63a57..8b7d286b 100644 --- a/crates/engine/src/cmd/xform.rs +++ b/crates/engine/src/cmd/xform.rs @@ -132,32 +132,29 @@ fn warp_node(n: &mut Node, pr: &Projective) { } fn distort(s: &mut Session, p: &Value) -> Result { - let corners: Vec = p + const C: &str = "object.distort"; + // All four corners or nothing, each exactly [x, y]: nothing malformed is left out. + let corners: Option> = p .get("corners") .and_then(Value::as_array) - .map(|a| a.iter().filter_map(|c| Some(Point::new(c.get(0)?.as_f64()?, c.get(1)?.as_f64()?))).collect()) - .unwrap_or_default(); - if corners.len() != 4 { - return Err(bad("object.distort", "corners must be 4 [x,y] points (TL, TR, BR, BL)")); + .filter(|a| a.len() == 4) + .and_then(|a| a.iter().map(|c| finite_numbers(c).map(|[x, y]| Point::new(x, y))).collect()); + let Some(&[tl, tr, br, bl]) = corners.as_deref() else { return Err(bad(C, "corners must be 4 [x, y] points (TL, TR, BR, BL)")) }; + let ids = if p.get("ids").is_some_and(|v| !v.is_null()) { checked_ids_param(s, p, "ids", C)? } else { selected_roots(s)? }; + if ids.is_empty() { + return Err(bad(C, "nothing to distort")); } - let ids = match ids_param(p, "ids") { - Some(v) => v, - None => selected_roots(s)?, - }; - let src = match p.get("from").and_then(Value::as_array) { - Some(a) if a.len() == 4 => { - let f: Vec = a.iter().filter_map(Value::as_f64).collect(); - if f.len() != 4 { - return Err(bad("object.distort", "from must be [x0,y0,x1,y1]")); - } - Rect::new(f[0], f[1], f[2], f[3]) + let src = match p.get("from").filter(|v| !v.is_null()) { + None => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| bad(C, "nothing to distort"))?, + Some(v) => { + let [x0, y0, x1, y1] = finite_numbers(v).ok_or_else(|| bad(C, "from must be [x0, y0, x1, y1]"))?; + Rect::new(x0, y0, x1, y1) } - _ => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| EngineError::Other("nothing to distort".into()))?, }; - if src.width().abs() < 1e-9 || src.height().abs() < 1e-9 { - return Err(EngineError::Other("cannot distort a zero-size bounding box".into())); + if !src.width().is_finite() || !src.height().is_finite() || src.width().abs() < 1e-9 || src.height().abs() < 1e-9 { + return Err(bad(C, "cannot distort a zero-size or non-finite bounding box")); } - let pr = Projective::from_rect(src, [corners[0], corners[1], corners[2], corners[3]]); + let pr = Projective::from_rect(src, [tl, tr, br, bl]); s.edit("Free Distort", |d, _| { for id in &ids { if let Some(n) = d.node_mut(*id) { diff --git a/crates/engine/src/tests_xform.rs b/crates/engine/src/tests_xform.rs index 0faf7cea..f5798ffb 100644 --- a/crates/engine/src/tests_xform.rs +++ b/crates/engine/src/tests_xform.rs @@ -572,3 +572,27 @@ fn copy_from_requires_source() { assert!(s.execute("appearance.copyFrom", &json!({})).is_err()); assert!(s.execute("appearance.copyFrom", &json!({"source": 9999})).is_err()); } + +#[test] +fn distort_rejects_malformed_coordinates_and_ids_without_editing_art() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let original = bounds(&s, a); + let history = undo_len(&s); + let corners = json!([[10, 0], [90, 0], [100, 100], [0, 100]]); + for p in [ + json!({"corners": [[10, 0, 50], [90, 0], [100, 100], [0, 100]]}), + json!({"corners": [[10, 0], [90, 0], ["bad", 100], [0, 100]]}), + json!({"corners": [[10, 0], [90, 0], [100, 100], [0, 100], ["bad"]]}), + json!({"corners": corners, "from": [0, 0, 100]}), + json!({"corners": corners, "from": "not bounds"}), + json!({"corners": corners, "ids": [a.0, "bad"]}), + json!({"corners": corners, "ids": [a.0, u64::MAX]}), + ] { + assert!(s.execute("object.distort", &p).is_err(), "invalid input: {p}"); + assert!(close(bounds(&s, a), original)); + assert_eq!(undo_len(&s), history); + } + s.execute("object.distort", &json!({"corners": corners, "from": [0, 0, 100, 100], "ids": [a.0]})).unwrap(); + assert_eq!(undo_len(&s), history + 1); +}