mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Plug-ins: a painted live effect's paint keys are checked at every level
A key that a paint does not have, such as `opacity` on a solid fill, was dropped when the paint was read, and the fill was drawn opaque. The result is now refused and `lastError` names the field, for keys on the paint and on its colors, gradient, stops, placement and freeform points.
This commit is contained in:
@@ -47,8 +47,8 @@ pub enum Scope {
|
||||
|
||||
/// Parses and checks a painted result: `{"objects": [...]}`, or `{"error": "message"}` when the
|
||||
/// plug-in declined. `rule` is the fill rule a piece without `fillRule` takes. Any other top-level
|
||||
/// key, an unknown key in a piece or stroke, a list or count past its limit, a number out of range
|
||||
/// or a paint of the wrong form refuses the whole result, naming the field.
|
||||
/// key, an unknown key in a piece, stroke or paint, a list or count past its limit, a number out of
|
||||
/// range or a paint of the wrong form refuses the whole result, naming the field.
|
||||
pub fn decode(bytes: &[u8], scope: Scope, rule: FillRule) -> Result<Vec<Piece>> {
|
||||
let v: Value = serde_json::from_slice(bytes).map_err(|e| out_err("output", format!("not JSON: {e}")))?;
|
||||
if let Some(e) = crate::objects::declined(&v) {
|
||||
@@ -162,12 +162,86 @@ fn color(c: &Color, at: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses a key of object `v` that is not in `keys`, naming it after `at`. A value that is not an
|
||||
/// object passes, and deserializing it reports the fault.
|
||||
fn only_keys(v: &Value, at: &str, keys: &[&str]) -> Result<()> {
|
||||
match v.as_object().and_then(|o| o.keys().find(|k| !keys.contains(&k.as_str()))) {
|
||||
Some(k) => Err(out_err(&format!("{at}.{k}"), format!("unknown key `{k}`; the keys here are {}", keys.join(", ")))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`only_keys`] for a color, whose keys follow its `model`. An unknown model passes, and
|
||||
/// deserializing it reports the fault.
|
||||
fn color_keys(v: &Value, at: &str) -> Result<()> {
|
||||
let keys: &[&str] = match v.get("model").and_then(Value::as_str) {
|
||||
Some("rgb") => &["model", "r", "g", "b"],
|
||||
Some("cmyk") => &["model", "c", "m", "y", "k"],
|
||||
Some("gray") => &["model", "k"],
|
||||
Some("lab") => &["model", "l", "a", "b"],
|
||||
_ => return Ok(()),
|
||||
};
|
||||
only_keys(v, at, keys)
|
||||
}
|
||||
|
||||
/// [`only_keys`] for a point `{x, y}`.
|
||||
fn point_keys(v: Option<&Value>, at: &str) -> Result<()> {
|
||||
v.map_or(Ok(()), |v| only_keys(v, at, &["x", "y"]))
|
||||
}
|
||||
|
||||
/// Calls `f` on each entry of the array at `v`, named `at[i]`.
|
||||
fn each(v: Option<&Value>, at: &str, f: impl Fn(&Value, &str) -> Result<()>) -> Result<()> {
|
||||
v.and_then(Value::as_array).into_iter().flatten().enumerate().try_for_each(|(i, e)| f(e, &format!("{at}[{i}]")))
|
||||
}
|
||||
|
||||
/// Refuses an unknown key at any level of paint `v`: the paint itself, its colors, a gradient's
|
||||
/// stops, placement and freeform points. Deserializing a paint drops unknown keys, so a key such
|
||||
/// as `opacity` on a solid paint would otherwise be accepted and do nothing.
|
||||
fn paint_keys(v: &Value, at: &str) -> Result<()> {
|
||||
match v.get("type").and_then(Value::as_str) {
|
||||
Some("none") => only_keys(v, at, &["type"]),
|
||||
Some("solid") => {
|
||||
only_keys(v, at, &["type", "color", "swatch", "tint"])?;
|
||||
v.get("color").map_or(Ok(()), |c| color_keys(c, &format!("{at}.color")))
|
||||
}
|
||||
Some("pattern") => only_keys(v, at, &["type", "pattern", "xf"]),
|
||||
Some("gradient") => {
|
||||
only_keys(v, at, &["type", "gradient", "geom", "angle", "swatch", "freeform"])?;
|
||||
if let Some(g) = v.get("gradient") {
|
||||
let at = format!("{at}.gradient");
|
||||
only_keys(g, &at, &["kind", "stops", "dither", "method"])?;
|
||||
each(g.get("stops"), &format!("{at}.stops"), |s, at| {
|
||||
only_keys(s, at, &["offset", "color", "opacity", "midpoint", "swatch", "tint", "plates"])?;
|
||||
s.get("color").map_or(Ok(()), |c| color_keys(c, &format!("{at}.color")))
|
||||
})?;
|
||||
}
|
||||
if let Some(geom) = v.get("geom") {
|
||||
let at = format!("{at}.geom");
|
||||
only_keys(geom, &at, &["start", "end", "aspect", "focal"])?;
|
||||
["start", "end", "focal"].into_iter().try_for_each(|k| point_keys(geom.get(k), &format!("{at}.{k}")))?;
|
||||
}
|
||||
if let Some(ff) = v.get("freeform") {
|
||||
let at = format!("{at}.freeform");
|
||||
only_keys(ff, &at, &["points", "lines", "mode"])?;
|
||||
each(ff.get("points"), &format!("{at}.points"), |p, at| {
|
||||
only_keys(p, at, &["at", "color", "opacity", "spread"])?;
|
||||
point_keys(p.get("at"), &format!("{at}.at"))?;
|
||||
p.get("color").map_or(Ok(()), |c| color_keys(c, &format!("{at}.color")))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A paint as the filter form has it (`null`: none), checked rather than clamped: colors, tints,
|
||||
/// stops, opacities, midpoints and placements in range.
|
||||
fn paint(v: &Value, at: &str) -> Result<Paint> {
|
||||
if v.is_null() {
|
||||
return Ok(Paint::None);
|
||||
}
|
||||
paint_keys(v, at)?;
|
||||
let p: Paint = serde_json::from_value(v.clone()).map_err(|e| out_err(at, e))?;
|
||||
let unit = |x: f32, at: &str, what: &str| in_range(x, 0.0, 1.0, at, what);
|
||||
match &p {
|
||||
|
||||
@@ -123,6 +123,67 @@ fn faults_refuse_the_whole_result_by_field() {
|
||||
assert_eq!(declined, Error::Failed("nothing to do".into()));
|
||||
}
|
||||
|
||||
/// A key a paint does not have, at any level of it, refuses the result and is named; it is not
|
||||
/// dropped.
|
||||
#[test]
|
||||
fn unknown_keys_inside_a_paint_are_refused_by_field() {
|
||||
let gray = json!({"model": "gray", "k": 0});
|
||||
let stop = |extra: Value| {
|
||||
let mut s = json!({"offset": 0, "color": gray});
|
||||
s.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": [s]}})
|
||||
};
|
||||
let point = json!({"x": 0, "y": 0});
|
||||
for (fill, at) in [
|
||||
(json!({"type": "solid", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}, "opacity": 0.5}), "objects[0].fills[0].opacity"),
|
||||
(json!({"type": "solid", "color": {"model": "gray", "k": 0, "alpha": 0.5}}), "objects[0].fills[0].color.alpha"),
|
||||
(json!({"type": "none", "color": gray}), "objects[0].fills[0].color"),
|
||||
(json!({"type": "pattern", "pattern": "Dots", "scale": 2}), "objects[0].fills[0].scale"),
|
||||
(stop(json!({"blend": "multiply"})), "objects[0].fills[0].gradient.stops[0].blend"),
|
||||
(stop(json!({"color": {"model": "lab", "l": 50, "a": 0, "b": 0, "c": 1}})), "objects[0].fills[0].gradient.stops[0].color.c"),
|
||||
(
|
||||
json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": [{"offset": 0, "color": gray}], "opacity": 0.5}}),
|
||||
"objects[0].fills[0].gradient.opacity",
|
||||
),
|
||||
(
|
||||
json!({"type": "gradient", "gradient": {"kind": "Linear", "stops": [{"offset": 0, "color": gray}]}, "opacity": 0.5}),
|
||||
"objects[0].fills[0].opacity",
|
||||
),
|
||||
(
|
||||
json!({"type": "gradient", "gradient": {"kind": "Radial", "stops": [{"offset": 0, "color": gray}]},
|
||||
"geom": {"start": point, "end": {"x": 1, "y": 0, "z": 0}}}),
|
||||
"objects[0].fills[0].geom.end.z",
|
||||
),
|
||||
(
|
||||
json!({"type": "gradient", "gradient": {"kind": "Freeform", "stops": [{"offset": 0, "color": gray}]},
|
||||
"freeform": {"points": [{"at": point, "color": gray, "glow": 1}]}}),
|
||||
"objects[0].fills[0].freeform.points[0].glow",
|
||||
),
|
||||
] {
|
||||
let e = decode(&out(json!([{"path": square(), "fills": [fill]}])), Scope::Object, FillRule::NonZero).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("{at}:")), "{at}: {e}");
|
||||
}
|
||||
let e = decode(
|
||||
&out(json!([{"path": square(), "strokes": [{"paint": {"type": "solid", "color": gray, "opacity": 0.5}}]}])),
|
||||
Scope::Object,
|
||||
FillRule::NonZero,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("objects[0].strokes[0].paint.opacity:"), "{e}");
|
||||
// Every key a paint does have is still taken.
|
||||
let full = json!({"type": "gradient", "angle": 30, "swatch": "Ramp",
|
||||
"gradient": {"kind": "Freeform", "dither": true, "method": "Perceptual",
|
||||
"stops": [{"offset": 0, "color": gray, "opacity": 1, "midpoint": 0.5, "swatch": "Ink", "tint": 0.5, "plates": [0, 0, 0, 0]}]},
|
||||
"geom": {"start": point, "end": {"x": 1, "y": 0}, "aspect": 1, "focal": point},
|
||||
"freeform": {"points": [{"at": point, "color": gray, "opacity": 1, "spread": 0.5}], "lines": [[0]], "mode": "lines"}});
|
||||
let solid = json!({"type": "solid", "color": {"model": "cmyk", "c": 0, "m": 0, "y": 0, "k": 1}, "swatch": "Ink", "tint": 0.5});
|
||||
let pattern = json!({"type": "pattern", "pattern": "Dots", "xf": [1, 0, 0, 1, 0, 0]});
|
||||
assert!(
|
||||
decode(&out(json!([{"path": square(), "fills": [full, solid, pattern, {"type": "none"}, null]}])), Scope::Object, FillRule::NonZero).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
/// The swatches and patterns a piece's paints name must be in the document.
|
||||
#[test]
|
||||
fn named_swatches_and_patterns_must_exist() {
|
||||
|
||||
+2
-1
@@ -413,7 +413,8 @@ is a piece drawn by itself, in the order returned, with paints of its own:
|
||||
|
||||
- `path` is required; `fillRule` defaults to the object's. `type` and `bounds` may be handed back as given and are
|
||||
ignored. Any other key (`opacity`, `effects`, `guide`, `locked`, `name`, a top-level `documentData`…) refuses the
|
||||
result.
|
||||
result, as does a key that a paint, its color, a gradient stop, a gradient's placement or a freeform point does not
|
||||
have, such as `opacity` on a solid paint.
|
||||
- `fills` and `strokes` take the forms a filter returns ([Input](#input)). A piece without `fills` (or `strokes`) is
|
||||
painted with the ones the effect was given; an empty list paints none. Given ones replace the object's in order, as
|
||||
a filter's output sets them: an existing fill or stroke keeps its own effects, opacity and blending, and a new stroke
|
||||
|
||||
Reference in New Issue
Block a user