diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 66b6d01df..16049850c 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -675,21 +675,25 @@ fn what_the_reader_doesnt_read_opens_as_before_with_a_warning() { /// 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. +/// can't be read is noted. With its layers read, the object a variable is bound to comes in bound, +/// and selecting a data set changes it; the bindings to objects the file doesn't have are noted. #[test] 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 square = format!("0 1 1 0 1 0 0 Xa\n{}f\n{}", ai::rect(10.0, 50.0, 40.0, 90.0), ai::named("path_x5F_1")); let notes = |r: &Value| { r["warnings"].as_array().map_or(vec![], |w| w.iter().filter(|w| w.as_str().unwrap().contains("variable")).cloned().collect::>()) }; - let check = |s: &mut Session| { + let skipped = |n: usize| { + json!(format!("the file's variable bindings weren't all read (bindings skipped: {n}): those objects aren't bound to their variables")) + }; + let check = |s: &mut Session, bound: usize| { 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_visible", "kind": "visibility", "bindings": bound}, {"name": "v_file", "kind": "image", "bindings": 0}, {"name": "v_graph", "kind": "graph", "bindings": 0}, {"name": "v_unbound", "kind": "unknown", "bindings": 0}, @@ -709,23 +713,34 @@ fn an_ai_file_opens_its_variable_library() { 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!(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(); + // The library's text and image variables are bound to objects the file doesn't have. + assert_eq!(notes(&r), [skipped(2)], "{r}"); + check(&mut s, 1); + let id = s.doc().unwrap().doc.layers[0].children().unwrap()[0].id; + assert_eq!(s.doc().unwrap().doc.variables.objects_of("v_visible"), [id]); + s.execute("dataset.select", &json!({"name": "ds_two"})).unwrap(); + assert!(!s.doc().unwrap().doc.node(id).unwrap().visible, "ds_two hides the square"); + s.execute("dataset.select", &json!({"name": "ds_one"})).unwrap(); + assert!(s.doc().unwrap().doc.node(id).unwrap().visible, "ds_one shows it"); + let e = s.execute("variable.bind", &json!({"variable": "v_unbound", "ids": [id.0]})).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. + // Its layers can't be read (a symbol): it comes in as its PDF part, with the library and none + // of its bindings. 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!(notes(&r).is_empty(), "{r}"); - check(&mut s); + assert_eq!(notes(&r), [skipped(3)], "{r}"); + check(&mut s, 0); // 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}"); + assert_eq!( + notes(&r), + [json!("the file's variable library was partly read (entries skipped: 1): the Variables panel lists the rest"), skipped(2)], + "{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(); diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 2e7d90aa6..4d1a328fd 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3354,6 +3354,42 @@ proptest! { survive("hostile .ai variable library values", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?; } + /// A `.ai` whose variable library binds objects by XML ids that repeat, name nothing, are empty + /// or are shared by several objects (a group and its path), with traits of every kind, then is + /// damaged: every object bound is one of the opened document's. + #[test] + fn ai_variable_bindings_never_panic( + cut in 0usize..20_000, + edits in prop::collection::vec((0usize..20_000, prop::sample::select(vec!['0', '1', ' ', '\n', '(', ')', '/', ':', ';', ',', '%', '_', 'Z', 'u', 'U'])), 0..12), + refs in prop::collection::vec((0usize..5, 0usize..5), 0..40), + art in prop::collection::vec(0usize..5, 0..12), + ) { + use vectorcraft_testkit::ai::{self, XmlValue}; + let names = ["a", "b", "a_x5F_1", "", "missing"]; + let traits = ["textcontent", "visibility", "fileref", "graphdata", "other"]; + let declared: Vec = refs + .iter() + .map(|&(t, n)| { + let uid = format!("/XMLUIDREF : ({}) ;", names[n]); + let attributes = [("trait", XmlValue::Str(traits[t])), ("varName", XmlValue::Unicode(traits[t])), ("docRef", XmlValue::Raw(&uid))]; + ai::xml_node(1, "variable", XmlValue::Str(""), &attributes, &[]) + }) + .collect(); + let set = ai::xml_element("variableSet", &[], &[ai::xml_element("variables", &[], &declared)]); + let library = ai::variable_library_data(&ai::xml_element("variableSets", &[], &[set])); + let art: String = art.iter().map(|&n| format!("u\n0 g\n10 10 m\n20 20 L\nS\n{}U\n{}", ai::named(names[n]), ai::named(names[(n + 1) % 5]))).collect(); + let data = ai::editing_data(100.0, 100.0, &format!("{library}{}", ai::layer("L", &art))); + let text = mutate_text(&data, cut, &edits); + let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&text)].concat(); + let mut bound = true; + survive("hostile .ai variable bindings", || { + let d = vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, true, 1.0).0; + bound = d.variables.bindings.keys().all(|id| d.node(*id).is_some()); + Some(d) + })?; + prop_assert!(bound, "a binding names an object the document doesn't have"); + } + /// A `.ai` whose Objects on Path record and group are damaged or hostile. #[test] fn ai_objects_on_path_never_panics(cut in 0usize..4_000, edits in prop::collection::vec((0usize..4_000, prop::sample::select(vec!['0', '1', '9', '-', '.', 'e', ' ', '\n', '(', ')', '/', ':', ';', ',', '%', '_', 'u', 'U'])), 0..12)) { diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index 75a9debf2..e413f6ec8 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -36,7 +36,9 @@ use crate::ps; pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; pub(crate) use frames::text_frames; -pub use read::{Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, brushes, palette, read, read_beside, variables}; +pub use read::{ + BindingRef, Brushes, IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, bindings_note, brushes, palette, read, read_beside, variables, +}; pub(crate) use read::{join_and, slot_frame, slot_of}; pub(crate) use read::is_look_slot; diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 885cc6a7c..6435c2dc6 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -117,7 +117,7 @@ mod types; pub(crate) use art::join_and; use art::preview_png; -pub use document::{Brushes, PaletteSwatches, VariableRead, artboards, brushes, palette, variables}; +pub use document::{BindingRef, Brushes, PaletteSwatches, VariableRead, artboards, bindings_note, brushes, palette, variables}; pub(crate) use document::{slot_frame, slot_of}; pub use types::IMAGE_TOO_LARGE; use types::{BLEND_BUDGET, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, Raster}; @@ -164,9 +164,37 @@ pub struct Structure { pub styles_read: bool, /// The file's `%AI24_LargeCanvasScale` (1 when it declares none), applied or not. pub canvas_scale: f64, + /// The objects by their XML id (`AI10_ArtUID`), a text object by its slot's id: `None` for an + /// id two objects have. + pub xml_ids: HashMap>, + /// The objects the variable library binds to its variables, by their XML ids: the importer + /// binds those it keeps. + pub bindings: Vec, + /// How many objects the library binds that aren't in `bindings` ([`VariableRead::bindings_skipped`]). + pub bindings_skipped: usize, pub warnings: Vec, } +/// The most XML ids of objects ([`Structure::xml_ids`]) kept. +const MAX_XML_IDS: usize = 1_000_000; + +/// The most bytes of XML ids ([`Structure::xml_ids`]) kept, all ids together. +pub(super) const MAX_XML_ID_BYTES: usize = 16 << 20; + +/// Record that the object `id` has the XML id `uid` in `xml_ids`, whose ids take `bytes` bytes. Two +/// objects with one id are neither of them that id's. A new id is left out when `xml_ids` holds +/// [`MAX_XML_IDS`] ids or its bytes would pass [`MAX_XML_ID_BYTES`]. +pub(super) fn remember_xml_id(xml_ids: &mut HashMap>, bytes: &mut usize, uid: String, id: NodeId) { + if let Some(seen) = xml_ids.get_mut(&uid) { + if *seen != Some(id) { + *seen = None; + } + } else if xml_ids.len() < MAX_XML_IDS && bytes.saturating_add(uid.len()) <= MAX_XML_ID_BYTES { + *bytes += uid.len(); + xml_ids.insert(uid, Some(id)); + } +} + /// Read the structure of the editing data `data`, applying the file's `%AI24_LargeCanvasScale` only /// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may /// keep the reduced one, which the PDF import reads as it is (#2031). @@ -323,6 +351,10 @@ struct Reader<'a> { obj_hidden: bool, /// The names of text objects, by their slot's id. slot_names: HashMap, + /// [`Structure::xml_ids`]. + xml_ids: HashMap>, + /// The bytes of the ids in `xml_ids`. + xml_id_bytes: usize, /// The artboard's size and centre in files without artboards or crop marks. art_size: Option<(f64, f64)>, template_center: Option<(f64, f64)>, @@ -456,6 +488,8 @@ impl<'a> Reader<'a> { done: false, obj_hidden: false, slot_names: HashMap::new(), + xml_ids: HashMap::new(), + xml_id_bytes: 0, art_size: None, template_center: None, large_canvas: None, diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index c5a05b915..5aebb94dd 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -331,17 +331,39 @@ fn artboards_read(data: &[u8]) -> Vec<(String, Rect)> { r.artboards.into_iter().map(|b| (b.name, b.rect)).collect() } +/// An object bound to a variable of the library: the variable's name, and the XML id of the object +/// (the `AI10_ArtUID` of its dictionary). The id is `None` when the reference can't be read. +#[derive(Clone, Debug, PartialEq)] +pub struct BindingRef { + pub variable: String, + pub target: Option, +} + /// The variable library of the editing data, as [`variable_library`] reads it. #[derive(Clone, Debug, Default, PartialEq)] pub struct VariableRead { /// Its variables, data sets, active data set and lock, without bindings. pub variables: Variables, + /// The objects bound to its variables, in the order the library declares them. + pub bindings: Vec, + /// How many objects its declarations bind that aren't in `bindings`: those of a declaration + /// left out, and those past [`MAX_LIBRARY_BINDINGS`]. + pub bindings_skipped: usize, /// How many of its declarations, data sets, values and settings were left out. pub skipped: usize, /// Whether the editing data has a variable library (a `variableSets` element). pub present: bool, } +/// The import's note about the library's bindings when `skipped` of them weren't made: nothing +/// when all of them were. +pub fn bindings_note(skipped: usize) -> Option { + // A count after a colon needs no plural, so every language can translate the note whole. + (skipped > 0).then(|| { + format!("the file's variable bindings weren't all read (bindings skipped: {skipped}): those objects aren't bound to their variables") + }) +} + impl VariableRead { /// The import's note about what of the library wasn't read: nothing when all of it was. pub fn note(&self) -> Option { @@ -521,6 +543,12 @@ impl<'a> Reader<'a> { { n.extras_mut().wrap = Some(wrap); } + // The object's XML id, which the variable library's bindings name it by. + if let Some(uid) = o.obj("AI10_ArtUID").and_then(Obj::content_text).filter(|u| !u.is_empty()) + && let Some(id) = self.last_object().filter(|n| !matches!(n.kind, NodeKind::Layer { .. })).map(|n| n.id) + { + super::remember_xml_id(&mut self.xml_ids, &mut self.xml_id_bytes, uid, id); + } let name = match o.text("AIArtName") { Some(name) => name, None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) { @@ -843,6 +871,7 @@ impl<'a> Reader<'a> { if variables.present { doc.variables = variables.variables.into(); } + let (bindings, bindings_skipped) = (variables.bindings, variables.bindings_skipped); Ok(Structure { doc, to_doc, @@ -858,6 +887,9 @@ impl<'a> Reader<'a> { envelopes_drawn: self.envelopes_drawn, styles_read, canvas_scale: self.canvas_scale, + xml_ids: self.xml_ids, + bindings, + bindings_skipped, warnings, }) } @@ -959,9 +991,10 @@ pub(super) fn find_entry<'o>(o: &'o Obj, key: &str, depth: usize) -> Option<&'o /// Caps on the variable library of the editing data, as Load Variable Library has them: the XML /// nodes read, the variables and data sets kept, the characters of a name, and the bytes of the -/// names and values kept. +/// names and values kept. Past those, the objects bound to the variables. const MAX_LIBRARY_NODES: usize = 2_000_000; const MAX_LIBRARY_VARIABLES: usize = 256; +const MAX_LIBRARY_BINDINGS: usize = 100_000; const MAX_LIBRARY_DATASETS: usize = 1_000; const MAX_LIBRARY_NAME: usize = 255; const MAX_LIBRARY_BYTES: usize = 8 << 20; @@ -974,11 +1007,13 @@ const MAX_LIBRARY_BYTES: usize = 8 << 20; /// are locked), with a `variables` element of `variable` elements (`varName`, and `trait`: /// `textcontent`, `visibility`, `fileref` or `graphdata`; another trait or none is a variable of /// unknown kind), and a `sampleDataSets` element (`activeDataSet`, the data set shown) of -/// `sampleDataSet` elements (`dataSetName`). In a data set, the element named after a variable holds -/// its value: `p` elements of text, one paragraph each; a `/Bool` text, whether its objects show; a -/// linked file's URL (`file://`) or path. Graph values aren't read. What is malformed, repeated or -/// past the caps (a declaration, a data set, a value, a setting, a second `variableSet`) is left out -/// and counted; a library past [`MAX_LIBRARY_NODES`] isn't read. Bindings aren't read. +/// `sampleDataSet` elements (`dataSetName`). A `variable` bound to an object has a `docRef` attribute +/// whose value is the object's XML id (`/XMLUIDREF`); a variable bound to several objects is +/// declared once for each, with the same name and trait. In a data set, the element named after a +/// variable holds its value: `p` elements of text, one paragraph each; a `/Bool` text, whether its +/// objects show; a linked file's URL (`file://`) or path. Graph values aren't read. What is +/// malformed, repeated or past the caps (a declaration, a data set, a value, a setting, a second +/// `variableSet`) is left out and counted; a library past [`MAX_LIBRARY_NODES`] isn't read. fn variable_library(o: &Obj) -> VariableRead { find_variable_sets(o, 0).map_or_else(VariableRead::default, read_variable_sets) } @@ -1113,11 +1148,23 @@ fn percent_decoded(s: &str) -> String { String::from_utf8(out).unwrap_or_else(|_| s.to_string()) } -/// The walk through a variable library: the nodes read, the bytes kept, and what was left out. +/// The object an element's `docRef` attribute names: `None` without one, `Some(None)` when its +/// value isn't an XML id (`/XMLUIDREF`) of at most [`MAX_LIBRARY_NAME`] characters. +fn doc_ref(o: &Obj) -> Option> { + let attributes = o.obj("xmlnode-attributes").filter(|a| a.ty == "Dictionary")?; + let node = attributes.items().find(|a| is_node(a, 2.0) && node_name(a).is_some_and(|n| n.rsplit(':').next() == Some("docRef")))?; + let id = node.obj("xmlnode-nodevalue").filter(|v| v.ty == "XMLUIDREF").and_then(Obj::content_text); + Some(id.filter(|id| !id.is_empty() && id.chars().count() <= MAX_LIBRARY_NAME)) +} + +/// The walk through a variable library: the nodes read, the bytes kept, the bindings, and what was +/// left out. #[derive(Default)] struct LibraryWalk { nodes: usize, bytes: usize, + bindings: Vec, + bindings_skipped: usize, skipped: usize, } @@ -1167,7 +1214,8 @@ impl LibraryWalk { } } - /// The `variable` elements of the `variables` element `list`, into `vars`. + /// The `variable` elements of the `variables` element `list`, into `vars`, and the objects they + /// bind. A second declaration of a variable binds one more object to it. fn declarations(&mut self, list: &Obj, vars: &mut Variables) { let Some(children) = self.children(list) else { self.skipped += 1; @@ -1175,10 +1223,6 @@ impl LibraryWalk { }; for v in children.into_iter().filter(|c| !is_node(c, 3.0)) { let name = is_element(v, "variable").then(|| library_name(xml_attribute(v, "varName"))).flatten(); - let Some(name) = name.filter(|n| vars.variable(n).is_none() && vars.variables.len() < MAX_LIBRARY_VARIABLES && self.keep(n.len())) else { - self.skipped += 1; - continue; - }; let kind = match xml_attribute(v, "trait").as_deref() { Some("textcontent") => VariableKind::Text, Some("visibility") => VariableKind::Visibility, @@ -1186,7 +1230,41 @@ impl LibraryWalk { Some("graphdata") => VariableKind::Graph, _ => VariableKind::Unknown, }; - vars.variables.push(Variable { name, kind }); + let target = doc_ref(v); + // A declaration left out, and the object it binds. + let left_out = |walk: &mut Self| { + walk.skipped += 1; + walk.bindings_skipped += usize::from(target.is_some()); + }; + let Some(name) = name else { + left_out(self); + continue; + }; + match vars.variable(&name) { + // Another object bound to the variable. + Some(declared) if declared.kind == kind => {} + // A second variable of that name, of another kind. + Some(_) => { + left_out(self); + continue; + } + None if vars.variables.len() < MAX_LIBRARY_VARIABLES && self.keep(name.len()) => { + vars.variables.push(Variable { name: name.clone(), kind }); + } + None => { + left_out(self); + continue; + } + } + if let Some(target) = target { + // The binding keeps its own copy of the name, and its target. + let len = name.len().saturating_add(target.as_ref().map_or(0, String::len)); + if self.bindings.len() < MAX_LIBRARY_BINDINGS && self.keep(len) { + self.bindings.push(BindingRef { variable: name, target }); + } else { + self.bindings_skipped += 1; + } + } } } @@ -1316,7 +1394,7 @@ fn read_variable_sets(sets: &Obj) -> VariableRead { None => walk.skipped += 1, } if walk.nodes > MAX_LIBRARY_NODES { - return VariableRead { variables: Variables::default(), skipped: 1, present: true }; + return VariableRead { variables: Variables::default(), bindings: vec![], bindings_skipped: 0, skipped: 1, present: true }; } - VariableRead { variables: vars, skipped: walk.skipped, present: true } + VariableRead { variables: vars, bindings: walk.bindings, bindings_skipped: walk.bindings_skipped, skipped: walk.skipped, present: true } } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index cbe7966e7..eb46288c4 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -1429,6 +1429,75 @@ fn ai_variable_library_definitions() { assert!(!r.warnings.iter().any(|w| w.contains("variable")), "{:?}", r.warnings); } +/// The objects a variable is bound to come with the library, each by the XML id its `docRef` gives, +/// in the order the library declares them: a variable bound to two objects is declared twice and +/// comes in once. A `docRef` that isn't an XML id is kept as unreadable, a declaration without one +/// binds nothing, and a second variable of the same name and another kind is left out with a note, +/// its binding counted as left out. +/// The objects read keep their XML ids; an id two objects have names neither of them. +#[test] +fn ai_variable_library_bindings() { + use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node}; + let declare = |name: &str, t: &str, art: Option| { + let mut attributes = vec![("trait", XmlValue::Str(t)), ("varName", XmlValue::Unicode(name))]; + attributes.extend(art.map(|a| ("docRef", a))); + xml_node(1, "variable", XmlValue::Str(""), &attributes, &[]) + }; + let declared = vec![ + declare("v_shown", "visibility", Some(XmlValue::Raw("/XMLUIDREF : (one) ;"))), + declare("v_text", "textcontent", Some(XmlValue::Str("one"))), + declare("v_shown", "visibility", Some(XmlValue::Raw("/XMLUIDREF : (two) ;"))), + declare("v_shown", "textcontent", Some(XmlValue::Raw("/XMLUIDREF : (two) ;"))), + declare("v_free", "visibility", None), + ]; + let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]); + let library = variable_library_data(&xml_element("variableSets", &[], &[set])); + let square = |x: f64, id: &str| format!("{RED}{}f\n{}", rect_path(x, 0.0, x + 10.0, 10.0), named(id)); + let squares = layer(&[square(0.0, "one"), square(20.0, "two"), square(40.0, "twice"), square(60.0, "twice")].concat()); + let r = read_ok(&format!("{library}{squares}")); + let names: Vec<&str> = r.doc.variables.variables.iter().map(|v| v.name.as_str()).collect(); + assert_eq!(names, ["v_shown", "v_text", "v_free"]); + let refs: Vec<(&str, Option<&str>)> = r.bindings.iter().map(|b| (b.variable.as_str(), b.target.as_deref())).collect(); + assert_eq!(refs, [("v_shown", Some("one")), ("v_text", None), ("v_shown", Some("two"))]); + assert!(r.warnings.contains(&partly_read(1)), "{:?}", r.warnings); + // The object the declaration left out binds is counted for the bindings' note. + assert_eq!(r.bindings_skipped, 1); + // The reader doesn't bind them: the importer does, with the objects it keeps. + assert!(r.doc.variables.bindings.is_empty()); + let ids: Vec = art(&r.doc).iter().map(|n| n.id).collect(); + assert_eq!(r.xml_ids.get("one"), Some(&Some(ids[0]))); + assert_eq!(r.xml_ids.get("two"), Some(&Some(ids[1]))); + assert_eq!(r.xml_ids.get("twice"), Some(&None)); +} + +/// The bindings count against the library's 8 MB of names and values: each one keeps its +/// variable's name and its target. The declarations past that bind nothing and are counted as left +/// out. +#[test] +fn ai_variable_library_bindings_stay_within_the_byte_budget() { + use vectorcraft_testkit::ai::{XmlValue, variable_library_data, xml_element, xml_node}; + let (name, id) = ("é".repeat(255), "a".repeat(255)); + let target = format!("/XMLUIDREF : ({id}) ;"); + let declare = || { + xml_node( + 1, + "variable", + XmlValue::Str(""), + &[("trait", XmlValue::Str("visibility")), ("varName", XmlValue::Unicode(&name)), ("docRef", XmlValue::Raw(&target))], + &[], + ) + }; + let declared: Vec<_> = (0..11_500).map(|_| declare()).collect(); + let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]); + let square = layer(&format!("{RED}{}f\n", rect_path(0.0, 0.0, 10.0, 10.0))); + let r = read_ok(&format!("{}{square}", variable_library_data(&xml_element("variableSets", &[], &[set])))); + assert_eq!(r.doc.variables.variables.len(), 1); + // The variable's name, then each binding's name and target. + let kept = ((8 << 20) - name.len()) / (name.len() + id.len()); + assert_eq!(r.bindings.len(), kept); + assert_eq!(r.bindings_skipped, 11_500 - kept); +} + /// The data sets come in in order with their values: text of one paragraph per `p` element (`&`, /// `<` and accented letters as they are), whether the objects show, and a linked file's path from /// its URL, kept when it names no file. A variable of unknown kind has no value. @@ -2164,6 +2233,34 @@ fn previewless_placement_placeholder_inherits_lock() { assert!(node.visible); } +/// A placed file's XML id names the linked image it comes in as, for the variable library's +/// bindings. +#[test] +fn a_placed_files_xml_id_names_its_image() { + let preview = [&b"2 1 12 32 17 "[..], &[255, 255, 0, 0, 128, 0, 64, 0, 9, 9, 9, 9]].concat(); + let name = "%_/ArtDictionary :\r%_/XMLUID : (photo_x5F_1) ; (AI10_ArtUID) ,\r%_;\r"; + let r = read(&placed(true, "[5 0 0 -5 0 200]", "/pics/photo.png", Some(&preview), name)).unwrap(); + let a = art(&r.doc); + assert!(matches!(a[0].kind, NodeKind::Image(_)), "{:?}", a[0].kind); + assert_eq!(r.xml_ids.get("photo_x5F_1"), Some(&Some(a[0].id))); +} + +/// The XML ids kept stay within `MAX_XML_ID_BYTES` in all: an id that would pass it is left out, +/// and a shorter one after it still fits. +#[test] +fn xml_ids_stay_within_their_byte_budget() { + use vectorcraft_doc::NodeId; + let (mut ids, mut bytes) = (std::collections::HashMap::new(), 0); + super::read::remember_xml_id(&mut ids, &mut bytes, "a".repeat(super::read::MAX_XML_ID_BYTES - 4), NodeId(1)); + super::read::remember_xml_id(&mut ids, &mut bytes, "long_id".into(), NodeId(2)); + super::read::remember_xml_id(&mut ids, &mut bytes, "id_2".into(), NodeId(3)); + super::read::remember_xml_id(&mut ids, &mut bytes, "id_2".into(), NodeId(4)); + assert_eq!(ids.len(), 2); + assert!(!ids.contains_key("long_id")); + assert_eq!(ids.get("id_2"), Some(&None), "two objects with one id are neither of them that id's"); + assert_eq!(bytes, super::read::MAX_XML_ID_BYTES); +} + #[test] fn placed_files_are_linked_images_showing_their_preview() { // A 2×1 preview (premultiplied ARGB, a padded row) of a file of 4×2 pixels. diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index 916451a38..354196555 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -32,7 +32,7 @@ use std::collections::BTreeSet; use std::sync::Arc; use vectorcraft_color::BlendMode; -use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, Scaling, TextKind, Unit}; +use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, Scaling, TextKind, Unit, VariableKind}; use vectorcraft_geom::{Affine, PathData, Point, Rect, Vec2}; use super::Imported; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 4226ccaac..31be3af5a 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -3,10 +3,12 @@ use super::compare::{ }; use super::plan::{made_only, make_slots, plan, readable_all}; use super::slots::{ - ART_LEFT_OUT, Content, DREW_DIFFERENTLY, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, - extent, few, fill_slots, letters, text_objects, words, + ART_LEFT_OUT, Content, DREW_DIFFERENTLY, Filled, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, + collect_slots, extent, few, fill_slots, letters, text_objects, words, +}; +use super::{ + Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, VariableKind, Vec2, ai, }; -use super::{Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, Vec2, ai}; /// Which box of the editing copy is the page. #[derive(Clone, Copy)] @@ -107,6 +109,9 @@ pub(super) fn build_from( envelopes_drawn, styles_read, canvas_scale, + xml_ids, + bindings, + bindings_skipped, warnings, } = structure; // Editing data in the units of the page's user space ([`in_units`]), or at its declared large @@ -282,7 +287,8 @@ pub(super) fn build_from( let mut empty = 0; let mut layers = std::mem::take(&mut doc.layers); let mut made = vec![]; - fill_slots(&mut doc, &mut layers, &mut content.into_iter(), &slot_names, &mut empty, &mut made, &mut vec![]); + let mut filled = std::collections::HashMap::new(); + fill_slots(&mut doc, &mut layers, &mut content.into_iter(), &slot_names, &mut empty, &mut made, &mut vec![], &mut filled); doc.layers = layers; made.sort(); let threaded_frames: BTreeSet = @@ -378,6 +384,7 @@ pub(super) fn build_from( None => unmark(&mut doc.layers, true, true, &mut vec![]), } doc.limit_depth_noting(&mut notes); + bind_variables(&mut doc, &bindings, bindings_skipped, &xml_ids, &filled, &mut notes); if small.is_some() { scale_document(&mut doc, unit, styles_read); relayout_wrapped(&mut doc); @@ -385,6 +392,66 @@ pub(super) fn build_from( Ok((doc, notes)) } +/// Bind the objects of `doc` to the variables of its library that `bindings` binds them to, each +/// object named by its XML id (`ids`: the object read with that id, or what `filled` made of a text +/// object's slot: for a visibility variable every object that took the slot's place, for a text +/// variable the one text object among them). An object is bound when it is of the variable's kind: +/// type for a text variable, a linked image for an image variable, any object for a visibility +/// variable. Locked and hidden objects, objects in locked or hidden layers and groups, and objects +/// on locked artboards keep their bindings. Graph variables and +/// variables of unknown kind bind nothing. The note counts the bindings that weren't made, with +/// `skipped` more that the reader left out. +fn bind_variables( + doc: &mut Document, + bindings: &[ai::BindingRef], + skipped: usize, + ids: &std::collections::HashMap>, + filled: &std::collections::HashMap, + notes: &mut Vec, +) { + let mut skipped = skipped; + if bindings.is_empty() { + notes.extend(ai::bindings_note(skipped)); + return; + } + let mut bound = vec![]; + for b in bindings { + let kind = doc.variables.variable(&b.variable).map(|v| v.kind); + let targets = match (b.target.as_ref().and_then(|t| ids.get(t).copied().flatten()), kind) { + (Some(id), Some(kind)) => match filled.get(&id) { + Some(f) if kind == VariableKind::Visibility => f.objects.clone(), + Some(f) => match f.text.as_slice() { + [one] => vec![*one], + _ => vec![], + }, + None => vec![id], + }, + _ => vec![], + }; + // Those still in the document: a part of a slot can have been taken out since. + let targets: Vec = targets.into_iter().filter(|id| doc.node(*id).is_some()).collect(); + let fits = |id: &NodeId| { + doc.node(*id).is_some_and(|n| match (kind, &n.kind) { + (Some(VariableKind::Visibility), _) => true, + (Some(VariableKind::Text), NodeKind::Text(_)) => true, + (Some(VariableKind::Image), NodeKind::Image(im)) => im.link.is_some(), + _ => false, + }) + }; + if !targets.is_empty() && targets.iter().all(fits) { + bound.push((targets, b.variable.as_str())); + } else { + skipped += 1; + } + } + for (targets, variable) in bound { + for id in targets { + doc.variables.bind(id, variable); + } + } + notes.extend(ai::bindings_note(skipped)); +} + /// Most anchors copied into the wrap shapes of a file's area type ([`vectorcraft_doc::text::wrap_work`]). const MAX_WRAP_WORK: usize = 2_000_000; @@ -1537,6 +1604,102 @@ mod candidate_tests { assert_eq!(wrap_shapes(&doc, text), 0); } + /// An image variable binds the linked image its XML id names, and not an embedded image or a + /// path; a text variable binds the type a slot became, and not a slot that became two text + /// objects; a visibility variable binds every object that took a slot's place and is still in + /// the document; a graph variable + /// binds nothing. A locked object, one in a hidden group and objects on a locked artboard are + /// bound too. Every binding left out, and those the reader left out, are counted in one note. + #[test] + fn variable_bindings_take_objects_of_their_kind() { + use vectorcraft_doc::{LinkInfo, Variable, Variables}; + let mut doc = Document::new(100.0, 100.0); + let made = insert_area(&mut doc, 0, Rect::new(0.0, 0.0, 50.0, 50.0)); + let (one, two) = (insert_area(&mut doc, 1, Rect::new(50.0, 0.0, 60.0, 10.0)), insert_area(&mut doc, 2, Rect::new(60.0, 0.0, 70.0, 10.0))); + let path = insert_wrap(&mut doc, Rect::new(0.0, 60.0, 10.0, 70.0), 4); + let locked = insert_wrap(&mut doc, Rect::new(20.0, 60.0, 30.0, 70.0), 4); + let image = |doc: &mut Document, link: Option| { + let id = doc.alloc_id(); + let im = ImageObject { key: "k".into(), width: 1, height: 1, xf: Affine::IDENTITY, link, placement: Default::default(), pixels: None }; + doc.insert(Some(doc.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap(); + id + }; + let (linked, embedded) = (image(&mut doc, Some(LinkInfo::new("/a.png"))), image(&mut doc, None)); + doc.node_mut(locked).unwrap().locked = true; + let hidden = doc.alloc_id(); + let inside = doc.alloc_id(); + let mut group = Node::group( + hidden, + vec![Arc::new(Node::new( + inside, + NodeKind::Image(Box::new(ImageObject { + key: "k".into(), + width: 1, + height: 1, + xf: Affine::IDENTITY, + link: Some(LinkInfo::new("/b.png")), + placement: Default::default(), + pixels: None, + })), + ))], + ); + group.visible = false; + doc.insert(Some(doc.layers[0].id), 0, group).unwrap(); + doc.artboards[0].locked = true; + let var = |name: &str, kind| Variable { name: name.into(), kind }; + doc.variables = Variables { + variables: vec![ + var("v_image", VariableKind::Image), + var("v_text", VariableKind::Text), + var("v_shown", VariableKind::Visibility), + var("v_graph", VariableKind::Graph), + ], + ..Default::default() + } + .into(); + // The slots the text objects came from are gone; their ids are the ones the reader gave. + let (slot, split) = (NodeId(9000), NodeId(9001)); + let ids: std::collections::HashMap> = [ + ("linked", Some(linked)), + ("embedded", Some(embedded)), + ("path", Some(path)), + ("locked", Some(locked)), + ("inside", Some(inside)), + ("slot", Some(slot)), + ("split", Some(split)), + ] + .map(|(k, v)| (k.to_string(), v)) + .into(); + let filled: std::collections::HashMap = [ + (slot, Filled { text: vec![made], objects: vec![made] }), + (split, Filled { text: vec![one, two], objects: vec![one, two, NodeId(9002)] }), + ] + .into(); + let bind = |variable: &str, target: &str| ai::BindingRef { variable: variable.into(), target: Some(target.into()) }; + let refs = [ + bind("v_image", "linked"), + bind("v_image", "embedded"), + bind("v_image", "path"), + bind("v_image", "inside"), + bind("v_text", "slot"), + bind("v_text", "split"), + bind("v_shown", "split"), + bind("v_shown", "locked"), + bind("v_graph", "path"), + ]; + let mut notes = vec![]; + bind_variables(&mut doc, &refs, 3, &ids, &filled, &mut notes); + let sorted = |mut v: Vec| { + v.sort(); + v + }; + assert_eq!(sorted(doc.variables.objects_of("v_image")), sorted(vec![linked, inside])); + assert_eq!(doc.variables.objects_of("v_text"), [made]); + assert_eq!(sorted(doc.variables.objects_of("v_shown")), sorted(vec![one, two, locked])); + assert!(doc.variables.objects_of("v_graph").is_empty()); + assert_eq!(notes, ai::bindings_note(4 + 3).into_iter().collect::>()); + } + /// Area type wrapped around a placed image flows around it at the image's other placement. #[test] fn wrap_shapes_follow_an_image_to_its_other_placement() { diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index f5f769380..bb88aa124 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -156,10 +156,12 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec, outl /// `doc` (a file read as its page or PDF part, in place of its layers) with the variables, data /// sets, active data set and lock of the variable library of its editing data `data`, as [`build`] /// gives a document read from the layers, and the note about what of the library wasn't read -/// added to `warnings`. Left as it is when the editing data has no library. +/// added to `warnings`: its bindings aren't, as the objects they bind aren't the layers' objects. +/// Left as it is when the editing data has no library. fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec) { let read = ai::variables(data); warnings.extend(read.note()); + warnings.extend(ai::bindings_note(read.bindings.len() + read.bindings_skipped)); if read.present { doc.variables = read.variables.into(); } diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index ed5b4c5a1..4924c167a 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -242,6 +242,25 @@ fn see_through(n: &Node) -> bool { n.opacity < 1.0 || n.blend != BlendMode::Normal || n.isolate } +/// What a text slot became in [`fill_slots`]: the text objects among it, those in groups included, +/// and the objects that took its place. +#[derive(Clone, Debug, Default, PartialEq)] +pub(super) struct Filled { + pub text: Vec, + pub objects: Vec, +} + +/// The ids of the text objects in `nodes` and in their groups, into `out`. +fn text_ids(nodes: &[Arc], out: &mut Vec) { + for n in nodes { + match n.children() { + _ if matches!(n.kind, NodeKind::Text(_)) => out.push(n.id), + Some(c) => text_ids(c, out), + None => {} + } + } +} + /// Replace the text slots in `nodes` with what `content` has for each, in order (a slot given none is /// left out): the page's type takes its place as it is, made type keeps the slot's name. Made type /// takes the slot's opacity, blend mode, isolation and knockout. The page's type takes them too when @@ -249,7 +268,9 @@ fn see_through(n: &Node) -> bool { /// combined with that of the objects around `nodes`; otherwise they keep their own. Where the slot has /// transparency and the type comes with other art that has none of its own, a group of them takes the /// slot's instead. `round` has the opacity and blend mode of each object around `nodes`, which the page -/// can have folded into its type. `empty` counts those left out. +/// can have folded into its type. `empty` counts those left out. `filled` gets what each slot +/// became, by the slot's id. +#[allow(clippy::too_many_arguments)] pub(super) fn fill_slots( doc: &mut Document, nodes: &mut Vec>, @@ -258,11 +279,13 @@ pub(super) fn fill_slots( empty: &mut usize, made: &mut Vec<(u32, usize, NodeId)>, round: &mut Vec<(f32, BlendMode)>, + filled: &mut std::collections::HashMap, ) { let mut i = 0; while i < nodes.len() { let Some(n) = nodes.get(i) else { break }; if let Some(story) = slot_of(n.name.as_deref()) { + let slot = n.id; let visible = n.visible; let name = names.get(&n.id); let frame = ai::slot_frame(n.name.as_deref()); @@ -290,6 +313,8 @@ pub(super) fn fill_slots( p }) .collect(); + let mut text = vec![]; + text_ids(&parts, &mut text); // When the page draws each of them with no transparency, the slot's, or the slot's // combined with that of the objects around it, the slot's takes its place: on the one // part, or on a group of several when the slot has transparency. Otherwise every part @@ -335,6 +360,7 @@ pub(super) fn fill_slots( { Arc::make_mut(p).extras_mut().graphic_style = linked; } + filled.insert(slot, Filled { text, objects: parts.iter().map(|p| p.id).collect() }); let n = parts.len(); nodes.splice(i..=i, parts); i += n; @@ -342,6 +368,7 @@ pub(super) fn fill_slots( } Some(Content::Made(mut text, _, seen)) => { text.id = doc.alloc_id(); + filled.insert(slot, Filled { text: vec![text.id], objects: vec![text.id] }); if let Some(story) = story { made.push((story, frame, text.id)); } @@ -392,7 +419,7 @@ pub(super) fn fill_slots( && let Some(c) = nodes.get_mut(i).map(Arc::make_mut).and_then(Node::children_mut) { round.push(state); - fill_slots(doc, c, content, names, empty, made, round); + fill_slots(doc, c, content, names, empty, made, round, filled); round.pop(); } i += 1; @@ -617,12 +644,36 @@ mod tests { let parts = vec![part(&mut doc, 10.0), part(&mut doc, 60.0)]; let mut nodes = vec![Arc::new(slot)]; let mut content = vec![Content::Page(parts)].into_iter(); - let (mut empty, mut made) = (0, vec![]); - fill_slots(&mut doc, &mut nodes, &mut content, &Default::default(), &mut empty, &mut made, &mut vec![]); + let (mut empty, mut made, mut filled) = (0, vec![], std::collections::HashMap::new()); + let slot = nodes[0].id; + fill_slots(&mut doc, &mut nodes, &mut content, &Default::default(), &mut empty, &mut made, &mut vec![], &mut filled); assert_eq!(nodes.len(), 1); assert_eq!(nodes[0].extras().wrap, Some(wrap)); let NodeKind::Group { children, .. } = &nodes[0].kind else { panic!("a group: {:?}", nodes[0].kind) }; assert_eq!(children.len(), 2); assert!(children.iter().all(|c| c.extras().wrap.is_none() && matches!(c.kind, NodeKind::Text(_)))); + // The slot became the group, which holds its two text objects. + assert_eq!(filled.get(&slot), Some(&Filled { text: children.iter().map(|c| c.id).collect(), objects: vec![nodes[0].id] })); + } + + /// A slot filled with the page's type in a group and a stroke beside it became both of them, and + /// the text object in the group. + #[test] + fn a_slot_filled_from_the_page_records_its_objects_and_its_type() { + let mut doc = Document::new(100.0, 100.0); + let mut slot = Node::group(doc.alloc_id(), vec![]); + slot.name = Some("\u{0}text0:0".into()); + let t = vectorcraft_doc::TextObject::point(Point::new(10.0, 50.0), "Part", Default::default()); + let text = Arc::new(Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)))); + let group = Arc::new(Node::group(doc.alloc_id(), vec![text])); + let stroke = Arc::new(Node::path(doc.alloc_id(), PathData::default(), Default::default())); + let mut nodes = vec![Arc::new(slot)]; + let slot = nodes[0].id; + let mut content = vec![Content::Page(vec![group, stroke])].into_iter(); + let (mut empty, mut made, mut filled) = (0, vec![], std::collections::HashMap::new()); + fill_slots(&mut doc, &mut nodes, &mut content, &Default::default(), &mut empty, &mut made, &mut vec![], &mut filled); + assert_eq!(nodes.len(), 2); + let inner = nodes[0].children().unwrap()[0].id; + assert_eq!(filled.get(&slot), Some(&Filled { text: vec![inner], objects: vec![nodes[0].id, nodes[1].id] })); } } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index e553712e9..a57177426 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1476,6 +1476,76 @@ fn ai_variable_library_pdf_fallback() { assert!(warnings.contains(¬e), "{warnings:?}"); let r = import(&eps(&page_square(10, 10), &art)).unwrap(); assert!(r.warnings.contains(¬e), "{:?}", r.warnings); + // The objects the library binds aren't those of the page: none is bound, and the note counts + // the library's three bindings. + let bindings = "the file's variable bindings weren't all read (bindings skipped: 3): those objects aren't bound to their variables".to_string(); + assert!(r.warnings.contains(&bindings), "{:?}", r.warnings); + assert!(r.document.variables.bindings.is_empty()); + let (d, warnings, _) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert!(warnings.contains(&bindings), "{warnings:?}"); + assert!(d.variables.bindings.is_empty()); +} + +/// The objects the variable library binds come in bound to its variables: type (made from the text +/// document), an object in a group, and a second object of the same variable, each found by its XML +/// id. A binding whose object isn't in the file, whose object isn't of the variable's kind (type +/// for a text variable), whose variable is of unknown kind, or whose reference can't be read binds +/// nothing, and the note counts them. Another object with the same id as the group is left alone. +#[test] +fn ai_variable_bindings_bind_the_objects_they_name() { + use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node}; + let declare = |name: &str, t: Option<&str>, art: Option| { + let mut attributes: Vec<(&str, XmlValue)> = t.map(|t| ("trait", XmlValue::Str(t))).into_iter().collect(); + attributes.push(("varName", XmlValue::Unicode(name))); + attributes.extend(art.map(|a| ("docRef", a))); + xml_node(1, "variable", XmlValue::Str(""), &attributes, &[]) + }; + let uid = |id: &str| format!("/XMLUIDREF : ({id}) ;"); + let declared = vec![ + declare("v_text", Some("textcontent"), Some(XmlValue::Raw(&uid("type_x5F_1")))), + declare("v_shown", Some("visibility"), Some(XmlValue::Raw(&uid("square_x5F_1")))), + declare("v_shown", Some("visibility"), Some(XmlValue::Raw(&uid("inner_x5F_1")))), + declare("v_wrong", Some("textcontent"), Some(XmlValue::Raw(&uid("square_x5F_1")))), + declare("v_gone", Some("visibility"), Some(XmlValue::Raw(&uid("nowhere")))), + declare("v_odd", None, Some(XmlValue::Raw(&uid("square_x5F_1")))), + declare("v_bad", Some("visibility"), Some(XmlValue::Str("square_x5F_1"))), + declare("v_twice", Some("visibility"), Some(XmlValue::Raw(&uid("twice")))), + declare("v_free", Some("visibility"), None), + ]; + let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]); + let library = variable_library_data(&xml_element("variableSets", &[], &[set])); + let back = format!( + "u\n{}\n{}U\n{}{}\n{}{}\n{}", + square(10, 10), + named("inner_x5F_1"), + named("twice"), + square(30, 10), + named("square_x5F_1"), + square(50, 10), + named("twice") + ); + let spare = format!("{}{}", text_object(0), named("type_x5F_1")); + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + // The type is on a layer that shows and doesn't print (its fourth flag off), which the page + // doesn't draw: it is made from the text document. + let spare = layer("Spare", true, &spare).replace("1 1 1 1 0 0 1 0 79", "1 1 1 0 0 0 1 0 79"); + let art = native_with_text(&(library + &layer("Back", true, &back) + &spare), &doc); + let page = [10, 30, 50].map(|x| page_square(x, 10)).join("\n"); + let r = import(&eps(&page, &art)).unwrap(); + assert_eq!(names(&r.document), ["Back", "Spare"], "{:?}", r.warnings); + let back = r.document.layers[0].children().unwrap(); + let inner = back[0].children().unwrap()[0].id; + let (square_id, type_id) = (back[1].id, r.document.layers[1].children().unwrap()[0].id); + assert!(matches!(r.document.node(type_id).map(|n| &n.kind), Some(NodeKind::Text(_))), "{:?}", r.warnings); + let vars = &r.document.variables; + assert_eq!(vars.objects_of("v_text"), [type_id], "{:?}", r.warnings); + let mut shown = vec![square_id, inner]; + shown.sort_unstable(); + assert_eq!(vars.objects_of("v_shown"), shown); + assert_eq!(vars.bindings.len(), 3, "{:?}", vars.bindings); + let note = "the file's variable bindings weren't all read (bindings skipped: 5): those objects aren't bound to their variables".to_string(); + assert!(r.warnings.contains(¬e), "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("variable library")), "{:?}", r.warnings); } /// An EPS whose editing data can't be used, a palette with no layers among others, comes in as its diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index fd683d3ce..cce9ebd4e 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5561,6 +5561,7 @@ theme Appearance Външен вид Effect Gallery Галерия с ефекти Filters Филтри Select artwork to apply effects Изберете графика, за да приложите ефекти +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables не всички връзки на променливите във файла бяха прочетени (пропуснати връзки: {_1}): тези обекти не са свързани със своите променливи @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest библиотеката с променливи на файла беше прочетена частично (пропуснати записи: {_1}): панелът „Променливи“ показва останалите @msg the file's variable library couldn't be read: the Variables panel doesn't list it библиотеката с променливи на файла не можа да бъде прочетена: панелът „Променливи“ не я показва @msg a variable of unknown kind can't be bound to objects променлива от неизвестен вид не може да бъде свързана с обекти diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index c785d37dd..098c02cd4 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5582,6 +5582,7 @@ mark-style Japanese Japanski Effect Gallery Galerija efekata Filters Filteri Select artwork to apply effects Odaberite crtež za primjenu efekata +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables nisu pročitane sve veze varijabli datoteke (preskočene veze: {_1}): ti objekti nisu vezani za svoje varijable @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest biblioteka varijabli datoteke je djelimično pročitana (preskočeni unosi: {_1}): panel Varijable navodi ostalo @msg the file's variable library couldn't be read: the Variables panel doesn't list it biblioteku varijabli datoteke nije bilo moguće pročitati: panel Varijable je ne navodi @msg a variable of unknown kind can't be bound to objects varijabla nepoznate vrste ne može se povezati s objektima diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5a591d27f..93fe3cb2b 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5556,6 +5556,7 @@ digits Malayalam Malaiàlam @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer no s’han llegit les capes del fitxer ({_1}): arriba com la seva pàgina, en una sola capa @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part no s’han llegit les capes del fitxer a partir de les seves dades d’edició ({_1}): provenen de la seva part PDF @msg the file's preview header points outside the file la capçalera de previsualització del fitxer apunta fora del fitxer +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables no s’han llegit totes les vinculacions de variables del fitxer (vinculacions omeses: {_1}): aquests objectes no estan vinculats a les seves variables @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest la biblioteca de variables del fitxer s’ha llegit només en part (entrades omeses: {_1}): el panell Variables llista la resta @msg the file's variable library couldn't be read: the Variables panel doesn't list it no s’ha pogut llegir la biblioteca de variables del fitxer: el panell Variables no la llista @msg a variable of unknown kind can't be bound to objects una variable de tipus desconegut no es pot vincular a objectes diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 1472c0747..67d5709b7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5670,6 +5670,7 @@ mark-style Japanese Japonské Effect Gallery Galerie efektů Filters Filtry Select artwork to apply effects Vyberte grafiku, na kterou chcete použít efekty +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables nebyly načteny všechny vazby proměnných souboru (vynechané vazby: {_1}): tyto objekty nejsou vázány na své proměnné @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest knihovna proměnných souboru byla načtena jen částečně (vynechané položky: {_1}): panel Proměnné zobrazuje zbytek @msg the file's variable library couldn't be read: the Variables panel doesn't list it knihovnu proměnných souboru nebylo možné načíst: panel Proměnné ji nezobrazuje @msg a variable of unknown kind can't be bound to objects proměnnou neznámého druhu nelze svázat s objekty diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 73256a62b..e45c0a364 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5243,6 +5243,7 @@ mark-style Japanese Japansk Path Length Stilængde @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer filens lag blev ikke læst ({_1}): den kommer ind som sin side, i ét lag @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part filens lag blev ikke læst fra dens redigeringsdata ({_1}): de kommer fra dens PDF-del +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables ikke alle filens variabelbindinger blev læst (udeladte bindinger: {_1}): de objekter er ikke bundet til deres variabler @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest filens variabelbibliotek blev kun delvist læst (udeladte poster: {_1}): variabelpanelet viser resten @msg the file's variable library couldn't be read: the Variables panel doesn't list it filens variabelbibliotek kunne ikke læses: variabelpanelet viser det ikke @msg a variable of unknown kind can't be bound to objects en variabel af ukendt type kan ikke bindes til objekter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c380e4a07..8db779ee4 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5250,6 +5250,7 @@ effect Soft Weich Effect Gallery Effekt-Galerie Filters Filter Select artwork to apply effects Wählen Sie Grafik aus, um Effekte anzuwenden +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables nicht alle Variablenbindungen der Datei wurden gelesen (übersprungene Bindungen: {_1}): Diese Objekte sind nicht an ihre Variablen gebunden @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest die Variablenbibliothek der Datei wurde nur teilweise gelesen (übersprungene Einträge: {_1}): Das Bedienfeld „Variablen“ listet den Rest auf @msg the file's variable library couldn't be read: the Variables panel doesn't list it die Variablenbibliothek der Datei konnte nicht gelesen werden: Das Bedienfeld „Variablen“ listet sie nicht auf @msg a variable of unknown kind can't be bound to objects eine Variable unbekannter Art kann nicht an Objekte gebunden werden diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c763a01f2..071628cc8 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5621,6 +5621,7 @@ effect Points Σημεία Effect Gallery Συλλογή εφέ Filters Φίλτρα Select artwork to apply effects Επιλέξτε γραφικά για να εφαρμόσετε εφέ +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables δεν διαβάστηκαν όλες οι συνδέσεις μεταβλητών του αρχείου (συνδέσεις που παραλείφθηκαν: {_1}): αυτά τα αντικείμενα δεν είναι συνδεδεμένα με τις μεταβλητές τους @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest η βιβλιοθήκη μεταβλητών του αρχείου διαβάστηκε μόνο εν μέρει (καταχωρίσεις που παραλείφθηκαν: {_1}): ο πίνακας Μεταβλητές εμφανίζει τις υπόλοιπες @msg the file's variable library couldn't be read: the Variables panel doesn't list it δεν ήταν δυνατή η ανάγνωση της βιβλιοθήκης μεταβλητών του αρχείου: ο πίνακας Μεταβλητές δεν την εμφανίζει @msg a variable of unknown kind can't be bound to objects μια μεταβλητή άγνωστου είδους δεν μπορεί να συνδεθεί με αντικείμενα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index d71d23b18..c63c6a07b 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5611,6 +5611,7 @@ mark-style Japanese Japonesas Effect Gallery Galería de efectos Filters Filtros Select artwork to apply effects Seleccione gráficos para aplicar efectos +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables no se leyeron todos los vínculos de variables del archivo (vínculos omitidos: {_1}): esos objetos no están vinculados a sus variables @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest la biblioteca de variables del archivo se leyó solo en parte (entradas omitidas: {_1}): el panel Variables muestra el resto @msg the file's variable library couldn't be read: the Variables panel doesn't list it no se pudo leer la biblioteca de variables del archivo: el panel Variables no la muestra @msg a variable of unknown kind can't be bound to objects una variable de tipo desconocido no se puede vincular a objetos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 102ef7c3b..03f290305 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5633,6 +5633,7 @@ mark-style Japanese Japanilainen Effect Gallery Tehostegalleria Filters Suodattimet Select artwork to apply effects Valitse kuvitus, johon tehosteet lisätään +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables kaikkia tiedoston muuttujasidoksia ei luettu (ohitettuja sidoksia: {_1}): niitä objekteja ei ole sidottu muuttujiinsa @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest tiedoston muuttujakirjasto luettiin vain osittain (ohitettuja kohtia: {_1}): Muuttujat-paneeli näyttää loput @msg the file's variable library couldn't be read: the Variables panel doesn't list it tiedoston muuttujakirjastoa ei voitu lukea: Muuttujat-paneeli ei näytä sitä @msg a variable of unknown kind can't be bound to objects tuntemattoman tyyppistä muuttujaa ei voi sitoa objekteihin diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index b1ddc5f4f..a0f15e3cc 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5665,6 +5665,7 @@ theme Appearance Apparence Effect Gallery Galerie d'effets Filters Filtres Select artwork to apply effects Sélectionnez des éléments graphiques pour appliquer des effets +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables les liaisons de variables du fichier n’ont pas toutes été lues (liaisons ignorées : {_1}) : ces objets ne sont pas liés à leurs variables @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest la bibliothèque de variables du fichier n’a été lue qu’en partie (entrées ignorées : {_1}) : le panneau Variables affiche le reste @msg the file's variable library couldn't be read: the Variables panel doesn't list it la bibliothèque de variables du fichier n’a pas pu être lue : le panneau Variables ne l’affiche pas @msg a variable of unknown kind can't be bound to objects une variable de type inconnu ne peut pas être liée à des objets diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7da6af288..e727d6aba 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5653,6 +5653,7 @@ imageTrace Outline Körvonal Effect Gallery Effektusgaléria Filters Szűrők Select artwork to apply effects Válasszon ki grafikát az effektusok alkalmazásához +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables a fájl változókötései közül nem mindegyik lett beolvasva (kihagyott kötések: {_1}): ezek az objektumok nincsenek a változóikhoz kötve @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest a fájl változókönyvtára csak részben lett beolvasva (kihagyott bejegyzések: {_1}): a Változók panel a többit listázza @msg the file's variable library couldn't be read: the Variables panel doesn't list it a fájl változókönyvtárát nem sikerült beolvasni: a Változók panel nem listázza @msg a variable of unknown kind can't be bound to objects ismeretlen típusú változó nem köthető objektumokhoz diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 4e32a3d8a..a15eddbb0 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5243,6 +5243,7 @@ mark-style Japanese Jepang Path Length Panjang jalur @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer lapisan file tidak terbaca ({_1}): file masuk sebagai halamannya, dalam satu lapisan @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part lapisan file tidak terbaca dari data pengeditannya ({_1}): lapisannya berasal dari bagian PDF file +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables tidak semua ikatan variabel file terbaca (ikatan yang dilewati: {_1}): objek-objek itu tidak terikat ke variabelnya @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest pustaka variabel file hanya terbaca sebagian (entri yang dilewati: {_1}): panel Variabel menampilkan sisanya @msg the file's variable library couldn't be read: the Variables panel doesn't list it pustaka variabel file tidak dapat dibaca: panel Variabel tidak menampilkannya @msg a variable of unknown kind can't be bound to objects variabel bertipe tidak dikenal tidak dapat diikat ke objek diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 56235fef1..4f93e7896 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5284,6 +5284,7 @@ lighting Light Luce Effect Gallery Galleria effetti Filters Filtri Select artwork to apply effects Seleziona la grafica a cui applicare gli effetti +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables non tutti i collegamenti delle variabili del file sono stati letti (collegamenti saltati: {_1}): quegli oggetti non sono collegati alle loro variabili @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest la libreria di variabili del file è stata letta solo in parte (voci saltate: {_1}): il pannello Variabili elenca il resto @msg the file's variable library couldn't be read: the Variables panel doesn't list it impossibile leggere la libreria di variabili del file: il pannello Variabili non la elenca @msg a variable of unknown kind can't be bound to objects una variabile di tipo sconosciuto non può essere associata a oggetti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index cc48dc8bb..806f1bcc3 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5327,6 +5327,7 @@ lighting Light ライト Effect Gallery 効果ギャラリー Filters フィルター Select artwork to apply effects 効果を適用するグラフィックを選択してください +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables ファイルの変数のバインドの一部が読み込まれませんでした (スキップしたバインド: {_1})。それらのオブジェクトは変数にバインドされていません @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest ファイルの変数ライブラリは一部だけ読み込まれました (スキップした項目: {_1})。残りは変数パネルに表示されます @msg the file's variable library couldn't be read: the Variables panel doesn't list it ファイルの変数ライブラリを読み込めませんでした。変数パネルには表示されません @msg a variable of unknown kind can't be bound to objects 種類が不明な変数はオブジェクトにバインドできません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 02470e035..855a8f6fc 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5544,6 +5544,7 @@ imageTrace Outline კონტური @msg no history to clear გასასუფთავებელი ისტორია არ არის @msg {_1} files are written by file.saveAsTemplate {_1} ფაილებს წერს file.saveAsTemplate @msg aiVersion 7 writes legacy .ai files, not .ait templates aiVersion 7 წერს ძველი ფორმატის .ai ფაილებს და არა .ait შაბლონებს +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables ფაილის ცვლადების ყველა კავშირი არ იქნა წაკითხული (გამოტოვებული კავშირები: {_1}): ეს ობიექტები არ არის დაკავშირებული თავიანთ ცვლადებთან @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest ფაილის ცვლადების ბიბლიოთეკა მხოლოდ ნაწილობრივ იქნა წაკითხული (გამოტოვებული ჩანაწერები: {_1}): პანელი „ცვლადები“ დანარჩენს აჩვენებს @msg the file's variable library couldn't be read: the Variables panel doesn't list it ფაილის ცვლადების ბიბლიოთეკის წაკითხვა ვერ მოხერხდა: პანელი „ცვლადები“ მას არ აჩვენებს @msg a variable of unknown kind can't be bound to objects უცნობი სახის ცვლადის ობიექტებთან დაკავშირება შეუძლებელია diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index fc30c96d3..04006d209 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5245,6 +5245,7 @@ lighting Light 광원 Effect Gallery 효과 갤러리 Filters 필터 Select artwork to apply effects 효과를 적용할 그래픽을 선택하세요 +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables 파일의 변수 바인딩 일부를 읽지 못했습니다(건너뛴 바인딩: {_1}). 해당 오브젝트는 변수에 바인딩되지 않습니다 @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest 파일의 변수 라이브러리를 일부만 읽었습니다(건너뛴 항목: {_1}). 나머지는 변수 패널에 표시됩니다 @msg the file's variable library couldn't be read: the Variables panel doesn't list it 파일의 변수 라이브러리를 읽을 수 없습니다. 변수 패널에 표시되지 않습니다 @msg a variable of unknown kind can't be bound to objects 종류를 알 수 없는 변수는 개체에 바인딩할 수 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index cd7a559d2..671f78da3 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5251,6 +5251,7 @@ mark-style Japanese Japans Path Length Padlengte @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer de lagen van het bestand zijn niet ingelezen ({_1}): het komt binnen als zijn pagina, in één laag @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part de lagen van het bestand zijn niet ingelezen uit de bewerkingsgegevens ({_1}): ze komen uit het PDF-gedeelte +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables niet alle variabelekoppelingen van het bestand zijn ingelezen (overgeslagen koppelingen: {_1}): die objecten zijn niet aan hun variabelen gekoppeld @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest de variabelenbibliotheek van het bestand is slechts gedeeltelijk ingelezen (overgeslagen items: {_1}): het Variabelen-paneel toont de rest @msg the file's variable library couldn't be read: the Variables panel doesn't list it de variabelenbibliotheek van het bestand kan niet worden ingelezen: het Variabelen-paneel toont die niet @msg a variable of unknown kind can't be bound to objects een variabele van onbekend type kan niet aan objecten worden gekoppeld diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d61832b34..cf9c4edf8 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5523,6 +5523,7 @@ mark-style Japanese Japońskie Effect Gallery Galeria efektów Filters Filtry Select artwork to apply effects Wybierz grafikę, aby zastosować efekty +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables nie wszystkie powiązania zmiennych pliku zostały wczytane (pominięte powiązania: {_1}): te obiekty nie są powiązane ze swoimi zmiennymi @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest biblioteka zmiennych pliku została wczytana tylko częściowo (pominięte wpisy: {_1}): panel Zmienne wyświetla resztę @msg the file's variable library couldn't be read: the Variables panel doesn't list it nie udało się wczytać biblioteki zmiennych pliku: panel Zmienne jej nie wyświetla @msg a variable of unknown kind can't be bound to objects zmiennej nieznanego rodzaju nie można powiązać z obiektami diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 31b2c5b93..d29cac871 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5585,6 +5585,7 @@ mark-style Japanese Japonesas Effect Gallery Galeria de Efeitos Filters Filtros Select artwork to apply effects Selecione a arte para aplicar efeitos +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables nem todos os vínculos de variáveis do arquivo foram lidos (vínculos ignorados: {_1}): esses objetos não estão vinculados às suas variáveis @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest a biblioteca de variáveis do arquivo foi lida só em parte (entradas ignoradas: {_1}): o painel Variáveis lista o restante @msg the file's variable library couldn't be read: the Variables panel doesn't list it não foi possível ler a biblioteca de variáveis do arquivo: o painel Variáveis não a lista @msg a variable of unknown kind can't be bound to objects uma variável de tipo desconhecido não pode ser vinculada a objetos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7a77d759b..13948efde 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5279,6 +5279,7 @@ lighting Light Свет Effect Gallery Галерея эффектов Filters Фильтры Select artwork to apply effects Выберите графику, чтобы применить эффекты +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables прочитаны не все привязки переменных файла (пропущено привязок: {_1}): эти объекты не привязаны к своим переменным @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest библиотека переменных файла прочитана только частично (пропущено записей: {_1}): панель «Переменные» показывает остальные @msg the file's variable library couldn't be read: the Variables panel doesn't list it не удалось прочитать библиотеку переменных файла: панель «Переменные» её не показывает @msg a variable of unknown kind can't be bound to objects переменную неизвестного вида нельзя связать с объектами diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 85e2cbb64..babde2822 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5692,6 +5692,7 @@ swatch Yellow Green Gulgrön Effect Gallery Effektgalleri Filters Filter Select artwork to apply effects Välj grafik för att tillämpa effekter +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables alla filens variabelbindningar lästes inte (överhoppade bindningar: {_1}): de objekten är inte bundna till sina variabler @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest filens variabelbibliotek lästes bara delvis (överhoppade poster: {_1}): panelen Variabler visar resten @msg the file's variable library couldn't be read: the Variables panel doesn't list it filens variabelbibliotek kunde inte läsas: panelen Variabler visar det inte @msg a variable of unknown kind can't be bound to objects en variabel av okänd typ kan inte kopplas till objekt diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 1e3574678..f2850ce92 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5436,6 +5436,7 @@ theme Appearance Görünüm @msg its zlib compression is damaged zlib sıkıştırması hasarlı @msg its Zstandard compression is damaged Zstandard sıkıştırması hasarlı @msg it decompresses to more than {_1} MB açıldığında {_1} MB'tan fazla alan kaplıyor +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables dosyanın değişken bağlantılarının tümü okunmadı (atlanan bağlantılar: {_1}): bu nesneler değişkenlerine bağlı değil @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest dosyanın değişken kitaplığı yalnızca kısmen okundu (atlanan girdiler: {_1}): Değişkenler paneli kalanları listeliyor @msg the file's variable library couldn't be read: the Variables panel doesn't list it dosyanın değişken kitaplığı okunamadı: Değişkenler paneli onu listelemiyor @msg a variable of unknown kind can't be bound to objects türü bilinmeyen bir değişken nesnelere bağlanamaz diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f0ba143cc..21b77da4b 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5264,6 +5264,7 @@ lighting Light Світло Effect Gallery Галерея ефектів Filters Фільтри Select artwork to apply effects Виберіть графіку, щоб застосувати ефекти +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables прочитано не всі прив’язки змінних файлу (пропущено прив’язок: {_1}): ці об’єкти не прив’язано до їхніх змінних @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest бібліотеку змінних файлу прочитано лише частково (пропущено записів: {_1}): панель «Змінні» показує решту @msg the file's variable library couldn't be read: the Variables panel doesn't list it не вдалося прочитати бібліотеку змінних файлу: панель «Змінні» її не показує @msg a variable of unknown kind can't be bound to objects змінну невідомого виду не можна прив’язати до об’єктів diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 436fe20d0..c456663aa 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5265,6 +5265,7 @@ theme Appearance Giao diện Effect Gallery Thư viện hiệu ứng Filters Bộ lọc Select artwork to apply effects Chọn đồ họa để áp dụng hiệu ứng +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables không phải mọi liên kết biến số của tệp đều được đọc (liên kết đã bỏ qua: {_1}): các đối tượng đó không được liên kết với biến số của chúng @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest thư viện biến số của tệp chỉ được đọc một phần (mục đã bỏ qua: {_1}): bảng Biến liệt kê phần còn lại @msg the file's variable library couldn't be read: the Variables panel doesn't list it không thể đọc thư viện biến số của tệp: bảng Biến không liệt kê nó @msg a variable of unknown kind can't be bound to objects không thể liên kết biến thuộc loại không xác định với đối tượng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2131de982..a55c343f2 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5507,6 +5507,7 @@ lighting Light 光源 Effect Gallery 效果库 Filters 滤镜 Select artwork to apply effects 选择要应用效果的图稿 +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables 未读取文件的全部变量绑定(跳过的绑定:{_1}):这些对象未绑定到其变量 @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest 文件的变量库只读取了一部分(跳过的条目:{_1}):“变量”面板列出其余部分 @msg the file's variable library couldn't be read: the Variables panel doesn't list it 无法读取文件的变量库:“变量”面板不会列出它 @msg a variable of unknown kind can't be bound to objects 类型未知的变量不能绑定到对象 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d2ce0501e..3eab26ac6 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2050,6 +2050,7 @@ file-filter Templates 範本 @msg the art on them differs from the page's in {_1}% of it: they come from its editing data all the same 其上的圖稿與頁面的圖稿有 {_1}% 不同:仍從其編輯資料匯入 @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer 未讀取檔案的圖層 ({_1}):檔案以其頁面匯入,位於單一圖層中 @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part 未從檔案的編輯資料讀取圖層 ({_1}):圖層來自其 PDF 部分 +@msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables 未讀取檔案的全部變數繫結 (略過的繫結:{_1}):這些物件未繫結至其變數 @msg the file's variable library was partly read (entries skipped: {_1}): the Variables panel lists the rest 只讀取了檔案的部分變數資料庫 (略過的項目:{_1}):「變數」面板會列出其餘部分 @msg the file's variable library couldn't be read: the Variables panel doesn't list it 無法讀取檔案的變數資料庫:「變數」面板不會列出它 @msg a variable of unknown kind can't be bound to objects 種類不明的變數無法繫結至物件 diff --git a/docs/mcp.md b/docs/mcp.md index 53a1b552d..398e0fdbd 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1705,7 +1705,9 @@ already walks Image Trace. Short agent entry points: `variable.bind`, `variable.unbind` and the `dataset.*` commands still run (except a `dataset.import` whose header would define a variable, below), and `variable.list` reports `locked`. A variable that an opened `.ai` declares without a kind VectorCraft reads is listed with `kind: "unknown"`: it takes no values, and `variable.bind` - refuses it. + refuses it. When an opened `.ai`'s layers are read, the objects it binds to its text, visibility and image + variables come in bound when they are of the variable's kind, locked and hidden objects included, and + `variable.list` counts them in `bindings`. - Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, newName?, values?}` (`newName` also renames the row, in the same undo step; Edit Data Set's Name field sends it), `dataset.rename`, `dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` / diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 200dc5c63..57e3defab 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1147,7 +1147,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In a `.ai` or EPS opened from its editing data, where a layer's or group's drawn look (an effect can be attached to a group or a layer, [Vector and raster effects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/vector-and-raster-effects.html)) draws a copy of art with type, the import puts the page's type that lies over the copy's other art into the copy's group, instead of giving it to other text objects by painting order. Type beside the copy's art, and every copy's type in a file past the import's text-object limits, still goes by painting order. Without a page, such a copy has no type, and no warning counts it as left out. How the page's type is matched with a copy is VectorCraft's own rule (`crates/eps/src/import/tests_native.rs`). A `.ai` opened from its editing data keeps Effect › Pathfinder on a group with no fills or strokes of its own as a live effect, with the group's members editable: Add, shown or hidden, in its order among the group's effects. The other operations, Pathfinder Options values other than the ones the reader accepts, and the effect on a path, on type, or on a fill or stroke open as the drawn look (`crates/eps/src/import/ai/style.rs`, `pathfinder_effect`). PDF text opens with every glyph where the file draws it (#722): the font's kerning or none, whichever the file followed, word and character spacing, `TJ` adjustments and horizontal scaling kept per character as tracking, text rise as baseline shift within its line, vertical glyphs narrower than an em centred on their column (`/W2`, `/W`, `/DW`), justified text boxes with word spacing back as justified area type (`tests_text_placement`, fixtures written from ISO 32000-1 §9). - A `.ai` or PDF whose editing data holds a readable variable library opens with its variable definitions, data-set rows, text, visibility and linked-file values, active row and lock in the Variables panel, whether its layers are read or it comes in as its PDF part; graph values and malformed entries are skipped with a note, and objects aren't bound yet (`crates/eps/src/import/ai/tests.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). + A `.ai` or PDF whose editing data holds a readable variable library opens with its variable definitions, data-set rows, text, visibility and linked-file values, active row and lock in the Variables panel, whether its layers are read or it comes in as its PDF part; graph values and malformed entries are skipped with a note. With its layers read, the objects its text, visibility and image variables are bound to (type for a text variable, any object for a visibility variable, a linked image for an image variable, found by their XML ids) come in bound, so selecting a data set changes them; locked and hidden objects, objects in a locked or hidden layer or group and objects on a locked artboard keep their bindings, and a data set leaves the locked ones as they are. Bindings whose object didn't come in or is of another kind are skipped with a note, as are graph bindings and every binding of a file that comes in as its PDF part (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` or EPS opened from its editing data keeps each artboard's center mark, cross hairs, video safe areas and video ruler pixel aspect ratio, and the file's Document Raster Effects Settings; the author comes from the header's `%%For` (the editing data's, else an EPS's own) when the file's File Info gives none, the header's custom colors that no swatch has come in as spot swatches, a named color the file defines takes the place of a default swatch of that name when the file has no palette, and an older file's `%%TemplateBox` places its artboard. A value of another type or out of range keeps its default (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`). A `.ai` opened from its editing data opens its Objects on Path groups live, with their path, their objects in order along it, Pivot and Rotate, and their art where the file draws it; a group whose spacing has been dragged, or with options an Objects on Path group doesn't hold, opens as a plain group (`crates/eps/src/import/ai/tests.rs`). A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art. A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. Pattern brushes and brushes that can't be read are left out with a note; brushed paths still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).