fix(distort): validate the full corner quad, source bounds and targets

This commit is contained in:
Nigel Ihza Firdausy 2026-10-10 23:57:02 +07:00 committed by bflatastic
parent 57050b0987
commit 0d697ed79d
2 changed files with 64 additions and 22 deletions

View File

@ -132,30 +132,48 @@ fn warp_node(n: &mut Node, pr: &Projective) {
}
fn distort(s: &mut Session, p: &Value) -> Result<Value> {
let corners: Vec<Point> = 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)"));
}
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<f64> = a.iter().filter_map(Value::as_f64).collect();
if f.len() != 4 {
return Err(bad("object.distort", "from must be [x0,y0,x1,y1]"));
const C: &str = "object.distort";
let invalid_corners = || bad(C, "corners must be exactly 4 finite [x,y] points (TL, TR, BR, BL)");
let points = p.get("corners").and_then(Value::as_array).filter(|a| a.len() == 4).ok_or_else(invalid_corners)?;
let corners: Vec<Point> = points
.iter()
.map(|v| {
let Some([x, y]) = v.as_array().map(Vec::as_slice) else {
return Err(invalid_corners());
};
let (Some(x), Some(y)) = (x.as_f64(), y.as_f64()) else {
return Err(invalid_corners());
};
if !x.is_finite() || !y.is_finite() {
return Err(invalid_corners());
}
Rect::new(f[0], f[1], f[2], f[3])
}
_ => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| EngineError::Other("nothing to distort".into()))?,
Ok(Point::new(x, y))
})
.collect::<Result<_>>()?;
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 src.width().abs() < 1e-9 || src.height().abs() < 1e-9 {
return Err(EngineError::Other("cannot distort a zero-size bounding box".into()));
if ids.is_empty() {
return Err(bad(C, "nothing to distort"));
}
let src = match p.get("from") {
None | Some(Value::Null) => s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| bad(C, "nothing to distort"))?,
Some(v) => {
let Some([x0, y0, x1, y1]) = v.as_array().map(Vec::as_slice) else {
return Err(bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]"));
};
let coordinate = |v: &Value| {
v.as_f64()
.filter(|n| n.is_finite())
.ok_or_else(|| bad(C, "from must be exactly 4 finite numbers [x0,y0,x1,y1]"))
};
Rect::new(coordinate(x0)?, coordinate(y0)?, coordinate(x1)?, coordinate(y1)?)
}
};
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]]);
s.edit("Free Distort", |d, _| {

View File

@ -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);
}