.ai import: open the variable library and data sets from the editing data

Squashed from #5156 onto main.
This commit is contained in:
dotCooCoo
2026-11-02 16:19:19 +07:00
committed by bflatastic
parent 952446253f
commit 8ab68e4223
42 changed files with 1096 additions and 148 deletions
+46 -13
View File
@@ -614,32 +614,65 @@ fn what_the_reader_doesnt_read_opens_as_before_with_a_warning() {
assert!(r["warnings"].to_string().contains("symbols") && r["warnings"].to_string().contains("PDF part"), "{r}");
}
/// A `.ai` whose document data has a variable library opens with a note that its variables and
/// data sets weren't read, whether its layers are read or it comes in as its PDF part.
/// A `.ai` whose document data has a variable library opens with its variables, data sets, active
/// data set and lock, whether its layers are read or it comes in as its PDF part: `variable.list`
/// and `dataset.list` report them. A variable of unknown kind can't be bound. What of the library
/// can't be read is noted.
#[test]
fn an_ai_file_with_a_variable_library_says_its_variables_werent_read() {
const NOTE: &str = "the file's variables and data sets weren't read (variables: 2, data sets: 3): the Variables panel doesn't list them";
let library = ai::variable_library(&["Name", "Number"], &["One", "Two", "Three"]);
fn an_ai_file_opens_its_variable_library() {
let library = ai::variable_library_data(&ai::sample_variable_sets("variableAndTypes", &[]));
let square = format!("0 1 1 0 1 0 0 Xa\n{}f\n", ai::rect(10.0, 50.0, 40.0, 90.0));
let notes =
|r: &Value| r["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("variables")).cloned().collect::<Vec<_>>();
let notes = |r: &Value| {
r["warnings"].as_array().map_or(vec![], |w| w.iter().filter(|w| w.as_str().unwrap().contains("variable")).cloned().collect::<Vec<_>>())
};
let check = |s: &mut Session| {
let r = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(
r,
json!({"variables": [
{"name": "v_text", "kind": "text", "bindings": 0},
{"name": "v_visible", "kind": "visibility", "bindings": 0},
{"name": "v_file", "kind": "image", "bindings": 0},
{"name": "v_graph", "kind": "graph", "bindings": 0},
{"name": "v_unbound", "kind": "unknown", "bindings": 0},
], "locked": true})
);
let r = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(r["active"], "ds_two", "{r}");
let names: Vec<&str> = r["datasets"].as_array().unwrap().iter().map(|d| d["name"].as_str().unwrap()).collect();
assert_eq!(names, ["ds_one", "ds_two", "ds_three"]);
assert_eq!(
r["datasets"][1]["values"],
json!({"v_text": {"text": "line one\nline two & < é"}, "v_visible": {"visible": false}, "v_file": {"image": ai::SAMPLE_MISSING_FILE}})
);
};
let mut s = Session::new();
let layered = ai::editing_data(200.0, 100.0, &format!("{library}{}", ai::layer("Art", &square)));
let file = ai::ai(&compressed(layered.as_bytes()), "1 0 0 rg 10 50 30 40 re f");
let r = open(&mut s, "variables.ai", &file, json!({})).unwrap();
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}");
assert_eq!(notes(&r), [json!(NOTE)], "{r}");
let r = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(r["variables"], json!([]), "{r}");
// Its layers can't be read (a symbol): it comes in as its PDF part, with the note.
assert!(notes(&r).is_empty(), "{r}");
check(&mut s);
let id = s.doc().unwrap().doc.layers[0].children().unwrap()[0].id.0;
let e = s.execute("variable.bind", &json!({"variable": "v_unbound", "ids": [id]})).unwrap_err();
assert!(e.to_string().contains("a variable of unknown kind can't be bound to objects"), "{e}");
// Its layers can't be read (a symbol): it comes in as its PDF part, with the library.
let symbols = ai::editing_data(200.0, 100.0, &format!("{library}{}", ai::layer("Art", "/SymbolInstance :\n(Dot) /SymbolRef ,\n;\n")));
let r = open(&mut s, "symbols.ai", &ai::ai(&compressed(symbols.as_bytes()), ai::page_pdf()), json!({})).unwrap();
assert!(r["warnings"].to_string().contains("PDF part"), "{r}");
assert_eq!(notes(&r), [json!(NOTE)], "{r}");
// Without a library, no note.
assert!(notes(&r).is_empty(), "{r}");
check(&mut s);
// A graph value isn't read: the note says how many entries were left out.
let graph = ai::xml_element("v_graph", &[], &[ai::xml_text(ai::XmlValue::Str("a,b"))]);
let partly = ai::variable_library_data(&ai::sample_variable_sets("none", &[graph]));
let layered = ai::editing_data(200.0, 100.0, &format!("{partly}{}", ai::layer("Art", &square)));
let r = open(&mut s, "graph.ai", &ai::ai(&compressed(layered.as_bytes()), "1 0 0 rg 10 50 30 40 re f"), json!({})).unwrap();
assert_eq!(notes(&r), [json!("the file's variable library was partly read (entries skipped: 1): the Variables panel lists the rest")], "{r}");
// Without a library, no note and no variables.
let plain = ai::editing_data(200.0, 100.0, &ai::layer("Art", &square));
let r = open(&mut s, "plain.ai", &ai::ai(&compressed(plain.as_bytes()), "1 0 0 rg 10 50 30 40 re f"), json!({})).unwrap();
assert!(notes(&r).is_empty(), "{r}");
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["variables"], json!([]));
}
/// An Objects on Path group in a `.ai` file opens as one: its record names the group's children by
+5 -1
View File
@@ -140,8 +140,11 @@ fn names_param(p: &Value, cmd: &str, fallback: Option<String>, nothing: &str) ->
/// Refuse what a variable of `kind` can't be bound to: art that is locked, on a hidden or locked
/// layer, or on a locked artboard; a text variable only drives type; an image variable only drives
/// an image object.
/// an image object; a variable of unknown kind drives nothing.
fn check_bindable(doc: &vectorcraft_doc::Document, ids: &[NodeId], kind: VariableKind, cmd: &str) -> Result<()> {
if kind == VariableKind::Unknown {
return Err(bad(cmd, "a variable of unknown kind can't be bound to objects"));
}
let locks = doc.locked_artboards();
for id in ids {
match doc.node(*id) {
@@ -189,6 +192,7 @@ fn dataset_value(cmd: &str, var: &str, kind: VariableKind, v: &Value) -> Result<
.map(str::trim)
.filter(|t| !t.is_empty())
.and_then(|t| if t.len() > MAX_GRAPH_CSV { None } else { Some(DataValue::Graph(t.to_string())) }),
VariableKind::Unknown => None,
}
.ok_or_else(|| bad(cmd, format!("value for `{var}` must match its variable kind")))
}
+2 -1
View File
@@ -203,6 +203,7 @@ fn cell_to_value(kind: VariableKind, cell: &str, quoted: bool, var: &str, line:
VariableKind::Graph if t.len() <= MAX_GRAPH_CSV => DataValue::Graph(t.to_string()),
VariableKind::Graph => return Err(bad(cmd, format!("line {line}: value for `{var}` is longer than {MAX_GRAPH_CSV} bytes"))),
VariableKind::Visibility => DataValue::Visible(parse_visible_cell(t, var, line, cmd)?),
VariableKind::Unknown => return Err(bad(cmd, format!("line {line}: value for `{var}` must match its variable kind"))),
}))
}
@@ -220,7 +221,7 @@ fn export_header(v: &Variable) -> String {
match v.kind {
VariableKind::Visibility => format!("#{}", v.name),
VariableKind::Image => format!("@{}", v.name),
VariableKind::Text | VariableKind::Graph => v.name.clone(),
VariableKind::Text | VariableKind::Graph | VariableKind::Unknown => v.name.clone(),
}
}