Merge pull request #6945 from dotCooCoo/feat/svg-variable-data

SVG: Save As SVG can include the document's variables and data sets
This commit is contained in:
BFlat
2026-11-13 03:28:53 +07:00
committed by GitHub
43 changed files with 735 additions and 17 deletions
File diff suppressed because one or more lines are too long
+29 -2
View File
@@ -398,7 +398,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
let editing = vectorcraft_svg::editing(&text).map(|e| (e.intact, move || vectorcraft_format::base64_decode(&e.data)));
// Relative links are found in the SVG's folder (when `name` is a path).
let folder = std::path::Path::new(name).parent().map(|f| f.to_string_lossy()).filter(|f| !f.is_empty()).map(|f| absolute_path(&f));
restore_or_import(editing, || import_svg(&text, folder.as_deref()))?
restore_or_import(editing, || open_svg(&text, folder.as_deref()))?
}
"pdf" | "ai" | "ait" => {
// The file is read and checked once, for the document it carries, its pages, the
@@ -573,6 +573,33 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
/// own): rasters stay linked, SVG files become art, missing ones placeholders (see
/// [`vectorcraft_svg::ImportOptions`]).
pub(crate) fn import_svg(text: &str, folder: Option<&str>) -> Result<(Document, Vec<String>)> {
import_svg_finding(text, folder, &std::collections::HashSet::new()).map(|(doc, warnings, _)| (doc, warnings))
}
/// [`import_svg`] for a document opened from an SVG: with the variable data the file carries (SVG
/// Options › Include Variable Data), read as [`crate::cmd::variablecmds::svg_library`] says. Variable
/// data that can't be read is left out with a warning; the art opens either way.
fn open_svg(text: &str, folder: Option<&str>) -> Result<(Document, Vec<String>)> {
let lib = match crate::cmd::variablecmds::svg_library(text) {
None => return import_svg(text, folder),
Some(Err(e)) => {
let (doc, mut warnings) = import_svg(text, folder)?;
warnings.push(format!("the file's variable data wasn't read: {e}"));
return Ok((doc, warnings));
}
Some(Ok(lib)) => lib,
};
let (mut doc, mut warnings, found) = import_svg_finding(text, folder, &lib.elements())?;
warnings.extend(crate::cmd::variablecmds::give_svg_library(&mut doc, lib, &found));
Ok((doc, warnings))
}
/// [`import_svg`], also returning the object made of each element whose id `ids` lists.
fn import_svg_finding(
text: &str,
folder: Option<&str>,
ids: &std::collections::HashSet<String>,
) -> Result<(Document, Vec<String>, vectorcraft_svg::Found)> {
// The importer reads on a thread of its own (for its stack): what may be read is what may be
// read here.
let scope = crate::file_access::ScopeHandle::capture();
@@ -588,7 +615,7 @@ pub(crate) fn import_svg(text: &str, folder: Option<&str>) -> Result<(Document,
Some((bytes, link))
})
};
vectorcraft_svg::import_with(text, &vectorcraft_svg::ImportOptions { folder, read: Some(&read) }).map_err(err)
vectorcraft_svg::import_finding(text, &vectorcraft_svg::ImportOptions { folder, read: Some(&read) }, ids).map_err(err)
}
/// Open a file's bytes as the new active document (what `document.open` does) →
+6
View File
@@ -90,6 +90,12 @@ pub const OPTIONS: &[FormatOption] = &[
default: "false",
description: "embed the fonts type uses as @font-face, subset to the characters used (whole when the font's licence forbids subsetting, left out with a warning when it forbids embedding)",
},
FormatOption {
name: "variableData",
ty: "boolean",
default: "false",
description: "Include Variable Data: write the text variables, the data sets and the type object each text variable is bound to as a variable library (variableSets) that document.open and variable.loadLibrary read back; variables of other kinds, and what is past the limits variable.loadLibrary reads (256 text variables, 1,000 data sets, 8 MiB), are left out with a warning",
},
FormatOption {
name: "svg",
ty: "object",
+1
View File
@@ -27,6 +27,7 @@ mod csv;
mod library;
use csv::{MAX_GRAPH_CSV, dataset_export, dataset_import};
pub(crate) use library::{give_svg_library, svg_library};
use library::{load_library, save_library};
/// The longest variable or dataset name, in characters (names arrive from MCP and files).
+170 -8
View File
@@ -1,6 +1,8 @@
use std::collections::HashMap;
use super::{
BTreeMap, BTreeSet, DataSet, DataValue, EngineError, MAX_NAME, Result, Session, Value, Variable, VariableKind, absolute_path, bad, json, named,
read_file_head, str_param, variables_unlocked, write_file,
BTreeMap, BTreeSet, DataSet, DataValue, Document, EngineError, MAX_NAME, NodeId, NodeKind, Result, Session, Value, Variable, VariableKind,
Variables, absolute_path, bad, json, named, read_file_head, str_param, variables_unlocked, write_file,
};
/// The namespace of a variable library's elements, the default namespace of its data sets' values,
@@ -169,12 +171,20 @@ fn without_doctype(xml: &str, cmd: &str) -> Result<String> {
}
/// A variable library as Load Variable Library reads it: the names of its text variables in the
/// order it declares them, the name and `trait` of each variable of another kind, and its data
/// sets with their text values.
struct Library {
/// order it declares them, the name and `trait` of each variable of another kind, its data sets
/// with their text values, and the element id each text variable's `docRef` names.
pub(crate) struct Library {
text: Vec<String>,
other: Vec<(String, String)>,
datasets: Vec<DataSet>,
refs: Vec<(String, String)>,
}
impl Library {
/// The element ids the text variables' `docRef`s name.
pub(crate) fn elements(&self) -> std::collections::HashSet<String> {
self.refs.iter().map(|(_, e)| e.clone()).collect()
}
}
/// Read a variable library: XML whose `variableSets` element holds `variableSet` elements, each
@@ -200,7 +210,7 @@ fn parse_library(xml: &str, cmd: &str) -> Result<Library> {
return Err(bad(cmd, "the file holds no `variableSets` element"));
};
let sets: Vec<roxmltree::Node> = root.children().filter(|n| is(n, "variableSet")).collect();
let (mut text, mut other, mut declared) = (Vec::new(), Vec::new(), BTreeSet::new());
let (mut text, mut other, mut declared, mut refs) = (Vec::new(), Vec::new(), BTreeSet::new(), Vec::new());
for var in sets.iter().flat_map(|s| s.children()).filter(|n| is(n, "variables")).flat_map(|v| v.children()).filter(|n| is(n, "variable")) {
let name = named(var.attribute("varName"), "varName", cmd)?;
if !declared.insert(name.clone()) {
@@ -210,7 +220,14 @@ fn parse_library(xml: &str, cmd: &str) -> Result<Library> {
return Err(bad(cmd, format!("the variable library declares more than {MAX_LIBRARY_VARIABLES} variables")));
}
match var.attribute("trait") {
Some("textcontent") => text.push(name),
Some("textcontent") => {
// `docRef="id('…')"` names the element of the variable's bound object.
let element = var.attribute("docRef").and_then(|r| r.trim().strip_prefix("id('")?.strip_suffix("')")).filter(|e| !e.is_empty());
if let Some(element) = element {
refs.push((name.clone(), element.to_string()));
}
text.push(name);
}
Some(t) => other.push((name, t.chars().take(MAX_NAME).collect())),
None => return Err(bad(cmd, format!("variable `{name}` has no `trait`"))),
}
@@ -253,7 +270,152 @@ fn parse_library(xml: &str, cmd: &str) -> Result<Library> {
}
datasets.push(DataSet { name, values });
}
Ok(Library { text, other, datasets })
Ok(Library { text, other, datasets, refs })
}
/// The variable library an SVG carries (SVG Options › Include Variable Data), read as
/// [`parse_library`] reads one, or `None` when the SVG holds no `variableSets` element. The
/// element is read on its own, from its start tag to the end tag that closes it
/// ([`variable_sets_range`]), with each reference to an
/// entity whose name starts with `ns_` replaced by the value the text before the element declares
/// for it (up to [`MAX_LIBRARY_ENTITIES`] of them, plain text of at most [`MAX_NAME`] bytes), as
/// files name the library's namespaces with such entities. The `ns_vars`, `ns_custom` and
/// `ns_flows` entities take the library's namespaces when the file doesn't declare them. An
/// element larger than [`MAX_LIBRARY_BYTES`], before or after the replacements, is refused.
pub(crate) fn svg_library(svg: &str) -> Option<Result<Library>> {
const C: &str = "document.open";
let (start, end) = variable_sets_range(svg)?;
let read = || {
let end = end.ok_or_else(|| bad(C, "the variable library declares no variables"))?;
let element = svg.get(start..end).unwrap_or_default();
if element.len() > MAX_LIBRARY_BYTES {
return Err(bad(C, format!("the variable library is larger than {MAX_LIBRARY_BYTES} bytes")));
}
let mut entities: Vec<(String, String)> = Vec::new();
for decl in svg.get(..start).unwrap_or_default().split("<!ENTITY").skip(1).take(MAX_LIBRARY_ENTITIES) {
let decl = decl.trim_start();
let Some((name, rest)) = decl.split_once(|c: char| c.is_ascii_whitespace()) else { continue };
let rest = rest.trim_start();
let Some(quote) = rest.chars().next().filter(|q| matches!(q, '"' | '\'')) else { continue };
let Some(value) = rest.get(1..).and_then(|v| v.split(quote).next()) else { continue };
if name.starts_with("ns_") && name.len() <= MAX_NAME && value.len() <= MAX_NAME && !value.contains(['<', '&', '%', '"']) {
entities.push((format!("&{name};"), value.to_string()));
}
}
for (name, value) in [("&ns_vars;", NS_VARIABLES), ("&ns_custom;", NS_CUSTOM), ("&ns_flows;", NS_FLOWS)] {
if !entities.iter().any(|(n, _)| n == name) {
entities.push((name.to_string(), value.to_string()));
}
}
// Counted before anything is replaced, so a hostile file can't make a large text first.
let grown =
entities.iter().map(|(name, value)| element.matches(name.as_str()).count() * value.len().saturating_sub(name.len())).sum::<usize>();
if element.len() + grown > MAX_LIBRARY_BYTES {
return Err(bad(C, format!("the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes")));
}
let mut element = element.to_string();
for (name, value) in &entities {
element = element.replace(name.as_str(), value);
}
parse_library(&format!("<svg>{element}</svg>"), C)
};
Some(read())
}
/// Where the first `variableSets` element of `svg` that is markup starts, and where the end tag
/// that closes it ends (`None` when no end tag does), or `None` when there is no such element.
/// Comments, CDATA sections, processing instructions and the document type declaration are
/// skipped, so text inside them that looks like the element is not taken for it.
fn variable_sets_range(svg: &str) -> Option<(usize, Option<usize>)> {
const NAME: &str = "variableSets";
// The element's name ends at white space, `>` or `/`.
let named = |rest: &str| rest.strip_prefix(NAME).is_some_and(|r| r.starts_with(|c: char| c.is_ascii_whitespace() || matches!(c, '>' | '/')));
if !svg.contains("<variableSets") {
return None;
}
let (mut at, mut start, mut depth) = (0, None, 0usize);
while let Some(off) = svg.get(at..).and_then(|s| s.find('<')) {
at += off;
let rest = svg.get(at..).unwrap_or_default();
// The end of a construct that holds text, or of the tag at `at`.
let past = |close: &str| rest.find(close).map(|e| at + e + close.len());
let next = if rest.starts_with("<!--") {
past("-->")
} else if rest.starts_with("<![CDATA[") {
past("]]>")
} else if rest.starts_with("<?") {
past("?>")
} else if rest.starts_with("<!DOCTYPE") {
// An internal subset ends at `]`, then the declaration at `>`.
match (rest.find('['), rest.find('>')) {
(Some(open), Some(gt)) if open < gt => {
rest.get(open..).and_then(|s| s.find(']')).and_then(|c| rest.get(open + c..)?.find('>').map(|g| at + open + c + g + 1))
}
(_, gt) => gt.map(|g| at + g + 1),
}
} else if let Some(close) = rest.strip_prefix("</") {
let tag_end = past(">");
if let Some(s) = start
&& named(close)
{
depth = depth.saturating_sub(1);
if depth == 0 {
return Some((s, tag_end));
}
}
tag_end
} else {
let tag_end = past(">");
if named(rest.get(1..).unwrap_or_default()) {
let empty = tag_end.and_then(|e| svg.get(..e)).is_some_and(|t| t.ends_with("/>"));
if start.is_none() {
start = Some(at);
}
if empty && depth == 0 {
return Some((at, tag_end));
}
depth += usize::from(!empty);
}
tag_end
};
let Some(next) = next else { break };
at = next;
}
start.map(|s| (s, None))
}
/// Give `doc`, opened from an SVG, the variable data `lib` it carries ([`svg_library`]): the
/// library's text variables, its data sets, no active data set, and each text variable bound to
/// the type object made of the element its `docRef` names, or the one type object among the
/// objects a group made of it holds (`found`: the object made of each element, by id). A `docRef`
/// that names no type object, or one another variable is bound to already, binds nothing. The art
/// stays as the file draws it. The warning about the variables of other kinds that were left out,
/// if any.
pub(crate) fn give_svg_library(doc: &mut Document, lib: Library, found: &HashMap<String, NodeId>) -> Option<String> {
let mut vars =
Variables { variables: lib.text.iter().map(|n| Variable { name: n.clone(), kind: VariableKind::Text }).collect(), ..Variables::default() };
vars.datasets = lib.datasets;
// A type object written with a paint of its own, or with fills and strokes of its appearance,
// is a group around its text (and the appearance's paths).
let text_of = |id: NodeId| match doc.node(id).map(|n| &n.kind) {
Some(NodeKind::Text(_)) => Some(id),
Some(NodeKind::Group { children, .. }) => {
let mut texts = children.iter().filter(|c| matches!(c.kind, NodeKind::Text(_)));
match (texts.next(), texts.next()) {
(Some(one), None) => Some(one.id),
_ => None,
}
}
_ => None,
};
for (name, element) in &lib.refs {
let Some(id) = found.get(element).and_then(|id| text_of(*id)) else { continue };
if !vars.bindings.contains_key(&id) {
vars.bind(id, name);
}
}
doc.variables = vars.into();
left_out_warning(lib.other.iter().map(|(_, t)| t.as_str()))
}
/// Load Variable Library: replace the document's variables and data sets with a variable library's
+188
View File
@@ -2916,3 +2916,191 @@ fn applying_a_graph_row_again_records_no_undo_step() {
s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!(super::history(&s), (before.0 + 1, 0, true));
}
/// The document's SVG with the options `svg` → (its text, the export's warnings).
fn svg_export(s: &mut Session, svg: Value) -> (String, Vec<Value>) {
let r = s.execute("document.export", &json!({"format": "svg", "svg": svg})).unwrap();
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
(String::from_utf8(bytes).unwrap(), r["warnings"].as_array().cloned().unwrap_or_default())
}
/// A new session with `svg` opened as `name` → (the session, the open's warnings).
fn svg_opened(svg: &str, name: &str) -> (Session, Vec<Value>) {
let mut o = Session::new();
let r = o.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(svg.as_bytes())})).unwrap();
let warnings = r["warnings"].as_array().cloned().unwrap_or_default();
(o, warnings)
}
/// Include Variable Data writes the text variables, the data sets' text values and each text
/// variable's bound type object into the SVG, and opening the SVG reads them back: the type
/// objects are bound again, so a data set changes them. Variables of other kinds are left out with
/// a warning. Unnamed objects get ids with Minimal ids, minified files, unique ids and style
/// entities alike, and Load Variable Library reads the same file.
#[test]
fn an_svg_with_variable_data_opens_with_its_variables_data_sets_and_bindings() {
let mut s = session();
let t = text(&mut s, "Alice");
let u = text(&mut s, "9.99");
// A fill of its appearance writes the type in a group with the fill's path.
s.execute("appearance.addFill", &json!({"ids": [u.0]})).unwrap();
let r = rect(&mut s);
for (name, kind) in [("Name", "text"), ("Shown", "visibility"), ("Price", "text")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Price", "ids": [u.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Shown", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A & <B>", "values": {"Name": "Bob", "Price": "1.00\nper pound & tax\r", "Shown": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Second", "values": {"Name": ""}})).unwrap();
let before = s.doc().unwrap().doc.variables.clone();
let text_only: Vec<vectorcraft_doc::DataSet> = before
.datasets
.iter()
.map(|d| {
let mut d = d.clone();
d.values.retain(|k, _| k != "Shown");
d
})
.collect();
for opts in [
json!({"variableData": true}),
json!({"variableData": true, "objectIds": "minimal", "minify": true}),
json!({"variableData": true, "objectIds": "unique"}),
json!({"variableData": true, "styling": "entities", "encoding": "latin1"}),
] {
let (svg, warnings) = svg_export(&mut s, opts.clone());
assert!(warnings.contains(&json!("Variables that aren't text variables were left out of the SVG's variable data")), "{opts}: {warnings:?}");
assert_eq!(svg.matches("docRef=\"id('").count(), 2, "{opts}: {svg}");
assert_eq!(s.doc().unwrap().doc.variables, before, "{opts}: exporting changes nothing");
let (mut o, warnings) = svg_opened(&svg, "vars.svg");
assert!(warnings.is_empty(), "{opts}: {warnings:?}");
let vars = o.doc().unwrap().doc.variables.clone();
assert_eq!(vars.variables.iter().map(|v| (v.name.as_str(), v.kind.label())).collect::<Vec<_>>(), [("Name", "text"), ("Price", "text")]);
assert_eq!(vars.datasets, text_only, "{opts}");
assert_eq!(vars.active_dataset, None);
let bound = |name: &str| {
let ids = vars.objects_of(name);
assert_eq!(ids.len(), 1, "{opts}: {name}");
ids[0]
};
let (name, price) = (bound("Name"), bound("Price"));
assert_eq!((plain(&o, name), plain(&o, price)), ("Alice".into(), "9.99".into()), "{opts}: the art stays as the file draws it");
let v = o.execute("dataset.select", &json!({"name": "A & <B>"})).unwrap();
assert_eq!(v["applied"], json!(2), "{opts}: {v}");
assert_eq!(plain(&o, name), "Bob");
let mut lib = session();
lib.execute("variable.loadLibrary", &json!({"xml": svg})).unwrap();
assert_eq!(lib.doc().unwrap().doc.variables.datasets, text_only, "{opts}");
}
}
/// Without Include Variable Data the SVG holds no variable data, and a document without variables
/// writes none with it on. Outlined type is written unbound, characters XML can't hold are left
/// out of the values, and variable data past the 8 MiB or 2,000,000-node cap is left out, each
/// with a warning; past 256 text variables or 1,000 data sets the rest are left out with a
/// warning. Look-alike text in comments and other markup is skipped when the file opens. A file
/// whose variable data can't be read opens its art with a warning, and so does one whose
/// namespace entities would grow the library past its cap.
#[test]
fn an_svg_without_readable_variable_data_opens_its_art() {
let mut s = session();
let t = text(&mut s, "Alice");
let (plain_svg, _) = svg_export(&mut s, json!({}));
assert!(!plain_svg.contains("variableSets"));
let (none, _) = svg_export(&mut s, json!({"variableData": true}));
assert!(!none.contains("variableSets"), "no variables, no variable data");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
let (off, _) = svg_export(&mut s, json!({}));
assert!(!off.contains("variableSets"), "off by default");
// Outlined type opens as art: the variable is written unbound, and the values lose the
// characters XML can't hold, each with a warning.
s.execute("dataset.new", &json!({"name": "Row", "values": {"Name": "a\u{b}b\u{1}c"}})).unwrap();
let (outlined, warnings) = svg_export(&mut s, json!({"variableData": true, "outlineText": true}));
assert!(outlined.contains("<variable varName=\"Name\"") && !outlined.contains("docRef"), "{outlined}");
for w in [
"Type written as outlines opens as art: its text variables were written unbound in the SVG's variable data",
"Characters XML can't hold were left out of data set values in the SVG's variable data",
] {
assert!(warnings.contains(&json!(w)), "{warnings:?}");
}
let (o, _) = svg_opened(&outlined, "outlined.svg");
let vars = &o.doc().unwrap().doc.variables;
assert!(vars.bindings.is_empty() && vars.variables.len() == 1, "{vars:?}");
assert_eq!(vars.datasets[0].values["Name"], vectorcraft_doc::DataValue::Text("a bc".into()));
// Variable data larger than Load Variable Library reads is left out with a warning.
s.execute("dataset.new", &json!({"name": "Huge", "values": {"Name": "x".repeat(8 << 20)}})).unwrap();
let (huge, warnings) = svg_export(&mut s, json!({"variableData": true}));
assert!(!huge.contains("variableSets"));
assert!(warnings.contains(&json!("The variable data is larger than 8 MiB, which Load Variable Library reads, and was left out of the SVG")));
let head = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\">";
let art = "<rect x=\"1\" y=\"1\" width=\"5\" height=\"5\"/></svg>";
let untraited = format!("{head}<variableSets><variableSet><variables><variable varName=\"A\"/></variables></variableSet></variableSets>{art}");
let refs = "&ns_big;".repeat(40_000);
let big = "x".repeat(250);
let growing = format!(
"<!DOCTYPE svg [<!ENTITY ns_big \"{big}\">]>{head}<variableSets><variableSet><variables><variable varName=\"A\" trait=\"textcontent\" category=\"{refs}\"/></variables></variableSet></variableSets>{art}"
);
// Variable data under 8 MiB with more XML nodes than Load Variable Library reads (a million
// one-character paragraphs, minified) is left out with a warning.
let mut lines = session();
lines.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
lines.execute("dataset.new", &json!({"name": "Row", "values": {"Name": "a\n".repeat(1_000_000)}})).unwrap();
let (dense, warnings) = svg_export(&mut lines, json!({"variableData": true, "minify": true}));
assert!(!dense.contains("variableSets"));
assert!(
warnings.contains(&json!(
"The variable data holds more than 2,000,000 XML nodes, which Load Variable Library reads, and was left out of the SVG"
)),
"{warnings:?}"
);
// More text variables and data sets than Load Variable Library reads: the first 256 and the
// first 1,000 are written, each with a warning, and the file opens with them.
let mut many = session();
{
let vars = &mut std::sync::Arc::make_mut(&mut many.doc_mut().unwrap().doc).variables;
vars.variables = (0..300).map(|i| vectorcraft_doc::Variable { name: format!("V{i}"), kind: vectorcraft_doc::VariableKind::Text }).collect();
vars.datasets = (0..1_100)
.map(|i| vectorcraft_doc::DataSet {
name: format!("D{i}"),
values: [("V0".to_string(), vectorcraft_doc::DataValue::Text(format!("{i}")))].into_iter().collect(),
})
.collect();
}
let (capped, warnings) = svg_export(&mut many, json!({"variableData": true}));
for w in [
"Text variables past the first 256, which Load Variable Library reads, were left out of the SVG's variable data",
"Data sets past the first 1,000, which Load Variable Library reads, were left out of the SVG's variable data",
] {
assert!(warnings.contains(&json!(w)), "{warnings:?}");
}
let (o, warnings) = svg_opened(&capped, "many.svg");
assert!(warnings.is_empty(), "{warnings:?}");
let vars = &o.doc().unwrap().doc.variables;
assert_eq!((vars.variables.len(), vars.datasets.len()), (256, 1_000));
assert_eq!(vars.datasets[999].values["V0"], vectorcraft_doc::DataValue::Text("999".into()));
// Text that only looks like variable data, in a comment, a CDATA section, a processing
// instruction or the document type declaration, is skipped for the element itself.
let library = "<variableSets><variableSet><variables><variable varName=\"A\" trait=\"textcontent\"/></variables></variableSet></variableSets>";
let decoys = format!(
"<!DOCTYPE svg [<!-- > <variableSets> -->]>{head}<!-- a > <variableSets> --><metadata><![CDATA[ a > <variableSets> ]]></metadata><?pi a > <variableSets> ?>{library}<!-- </variableSets> -->{art}"
);
let (o, warnings) = svg_opened(&decoys, "decoys.svg");
assert!(warnings.is_empty(), "{warnings:?}");
assert_eq!(o.doc().unwrap().doc.variables.variables.len(), 1);
for (svg, why) in [(untraited, "variable `A` has no `trait`"), (growing, "entities expand to more than")] {
let (o, warnings) = svg_opened(&svg, "broken.svg");
assert!(warnings.iter().any(|w| w.as_str().is_some_and(|w| w.contains("variable data wasn't read") && w.contains(why))), "{warnings:?}");
let d = &o.doc().unwrap().doc;
assert!(d.variables.variables.is_empty());
assert_eq!(vectorcraft_testkit::svg_shapes(&vectorcraft_svg::export(d, &Default::default())), 1, "the art opens");
}
}
+9
View File
@@ -26,6 +26,7 @@ mod nodes;
mod output;
mod paint;
mod text;
mod variables;
use nodes::SymbolDef;
pub(crate) use nodes::{MASK_FLAGS, Reuse};
@@ -117,8 +118,10 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
layer_nest: 0,
non_latin1_names: false,
foreign_ns: BTreeMap::new(),
bound: HashMap::new(),
};
w.assign_name_ids();
w.assign_bound_ids();
for layer in &doc.layers {
w.count_colorized_images(layer);
}
@@ -135,6 +138,8 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
w.line("</g>");
}
// Include Variable Data, once the body has written the bound objects' ids.
let variables = w.variable_sets();
// XML names cannot use character references. Preserve names that Latin-1 cannot encode.
let encoding = if opts.encoding == Encoding::Latin1 && w.non_latin1_names {
w.warn("Unicode data attribute names require UTF-8; SVG was written in UTF-8");
@@ -207,6 +212,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
out.push_str(&w.indent(1));
out.push_str(&format!("<script xlink:href=\"{}\"/>{nl}", xml_attr_escape(file)));
}
out.push_str(&variables);
let latin1 = encoding == Encoding::Latin1;
if latin1 {
latin1_refs(&mut out);
@@ -302,4 +308,7 @@ struct Writer<'a> {
/// The prefix written for each namespace of other applications' attributes, by its URI
/// ([`Writer::foreign_attrs`]): declared on the root.
foreign_ns: BTreeMap<String, String>,
/// Include Variable Data: the objects text variables are bound to, with the element id each
/// was written with, once written ([`Writer::note_bound`]).
bound: HashMap<NodeId, variables::Bound>,
}
+4 -1
View File
@@ -30,7 +30,7 @@ pub(super) fn valid_xml_value(s: &str) -> bool {
}
/// An XML name without a colon (an NCName): a letter or `_`, then [`xml_name_char`]s.
fn xml_ncname(s: &str) -> bool {
pub(super) fn xml_ncname(s: &str) -> bool {
let mut chars = s.chars();
chars.next().is_some_and(|c| c.is_alphabetic() || c == '_') && chars.all(xml_name_char)
}
@@ -193,6 +193,9 @@ impl Writer<'_> {
return String::new();
}
let mut out = self.names.get(&n.id).map_or_else(String::new, |id| format!(" id=\"{}\"", xml_escape(id)));
if let Some(id) = self.names.get(&n.id).filter(|_| !self.bound.is_empty()).cloned() {
self.note_bound(n.id, &id);
}
if let (Some(id), Some(name)) = (self.names.get(&n.id), n.name.as_deref())
&& name != id
{
+1
View File
@@ -165,6 +165,7 @@ impl Writer<'_> {
}
}
let id = self.id_attr(n);
self.note_outlined(n.id);
let a = self.attrs(&css::transparency(n));
// Text outlined because SVG text can't draw it (bent glyphs, vertical type, ruby, effects on
// a path and the like) names its text for screen readers and search; Convert to Outlines
+223
View File
@@ -0,0 +1,223 @@
//! Include Variable Data: the document's text variables, its data sets and the object each text
//! variable is bound to, written as a `variableSets` element in the layout Load Variable Library
//! reads (`variableSets` › `variableSet` › `variables` of `variable` elements, and
//! `sampleDataSets` of `sampleDataSet` elements). A variable's bound object is named by its
//! element's id in a `docRef` attribute, `docRef="id('…')"`.
use std::collections::HashSet;
use vectorcraft_doc::{DataValue, Document, NodeId, NodeKind, VariableKind};
use super::Writer;
use super::output::{valid_xml_value, xml_attr_escape, xml_ncname, xml_text};
/// The namespace of a variable library's elements.
const NS_VARIABLES: &str = "http://ns.adobe.com/Variables/1.0/"; // brand-ok: the namespace variable libraries use
/// The default namespace inside `sampleDataSets`, which the elements holding values are in.
const NS_CUSTOM: &str = "http://ns.adobe.com/GenericCustomNamespace/1.0/"; // brand-ok: the namespace variable libraries use
/// The `category` of a text variable.
const NS_FLOWS: &str = "http://ns.adobe.com/Flows/1.0/"; // brand-ok: the namespace variable libraries use
/// The most variable data a file holds: the largest variable library Load Variable Library reads.
const MAX_VARIABLE_DATA: usize = 8 << 20;
/// The most variables a variable library Load Variable Library reads declares.
const MAX_VARIABLES: usize = 256;
/// The most data sets a variable library Load Variable Library reads holds.
const MAX_DATASETS: usize = 1_000;
/// The most XML nodes (elements and text) a variable library Load Variable Library reads holds.
const MAX_NODES: usize = 2_000_000;
/// The text variables of `doc` that a variable library can hold: those whose names can name an
/// element, the first [`MAX_VARIABLES`] of them.
fn written_variables(doc: &Document) -> Vec<&str> {
nameable_variables(doc).take(MAX_VARIABLES).collect()
}
/// The text variables of `doc` whose names can name an element.
fn nameable_variables(doc: &Document) -> impl Iterator<Item = &str> {
doc.variables.variables.iter().filter(|v| v.kind == VariableKind::Text && xml_ncname(&v.name)).map(|v| v.name.as_str())
}
/// The object each text variable of `doc` that a library can hold names: the first of its bound
/// objects (by object id) that is a type object and that no variable before it names.
fn bound_objects(doc: &Document) -> Vec<(&str, NodeId)> {
let mut taken = HashSet::new();
written_variables(doc)
.into_iter()
.filter_map(|name| {
let id = doc
.variables
.objects_of(name)
.into_iter()
.find(|id| !taken.contains(id) && doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Text(_))))?;
taken.insert(id);
Some((name, id))
})
.collect()
}
impl Writer<'_> {
/// With Include Variable Data, give each object a text variable names an id when its name gave
/// it none (an unnamed object, or Minimal object ids). Called after the named objects' ids are
/// taken.
pub(super) fn assign_bound_ids(&mut self) {
if !self.opts.variable_data {
return;
}
let doc = self.doc;
for (name, id) in bound_objects(doc) {
if !self.names.contains_key(&id) {
let cand = self.unique_id(&super::sanitize_id(name));
self.names.insert(id, cand);
}
self.bound.insert(id, Bound::Pending);
}
}
/// Note that the object `id` was written with the element id `element`, when a variable names
/// it (the first element written for it).
pub(super) fn note_bound(&mut self, id: NodeId, element: &str) {
if let Some(slot) = self.bound.get_mut(&id)
&& *slot == Bound::Pending
{
*slot = Bound::Written(element.to_string());
}
}
/// Note that the type object `id` was written as outlines, which open as art rather than type.
pub(super) fn note_outlined(&mut self, id: NodeId) {
if let Some(slot) = self.bound.get_mut(&id) {
*slot = Bound::Outlined;
}
}
/// The `variableSets` element, indented one level, or nothing when the option is off or the
/// document has no text variable a library can hold. Variables of other kinds, names that
/// can't name an element, and data sets whose names are blank, have white space at either end,
/// hold characters XML can't hold or repeat a name are left out, each kind with a warning. A
/// variable whose object this file doesn't hold, or holds as outlines, is written without a
/// `docRef` (outlines with a warning). Characters XML can't hold are left out of the values,
/// with a warning. Variable data larger than Load Variable Library reads
/// ([`MAX_VARIABLE_DATA`]) is left out with a warning.
pub(super) fn variable_sets(&mut self) -> String {
let doc = self.doc;
let vars = &doc.variables;
if !self.opts.variable_data || vars.variables.is_empty() {
return String::new();
}
if vars.variables.iter().any(|v| v.kind != VariableKind::Text) {
self.warn("Variables that aren't text variables were left out of the SVG's variable data");
}
if vars.variables.iter().any(|v| v.kind == VariableKind::Text && !xml_ncname(&v.name)) {
self.warn("Text variables whose names can't name an XML element were left out of the SVG's variable data");
}
let names = written_variables(doc);
if names.is_empty() {
return String::new();
}
if nameable_variables(doc).nth(MAX_VARIABLES).is_some() {
self.warn("Text variables past the first 256, which Load Variable Library reads, were left out of the SVG's variable data");
}
// XML names can't be written as character references.
self.non_latin1_names |= names.iter().any(|n| n.chars().any(|c| u32::from(c) > 0xff));
let bound = bound_objects(doc);
let slots: Vec<Option<&Bound>> =
names.iter().map(|name| bound.iter().find(|(n, _)| n == name).and_then(|(_, id)| self.bound.get(id))).collect();
let outlined = slots.contains(&Some(&Bound::Outlined));
let refs: Vec<Option<String>> = slots
.into_iter()
.map(|s| match s {
Some(Bound::Written(e)) => Some(e.clone()),
_ => None,
})
.collect();
if outlined {
self.warn("Type written as outlines opens as art: its text variables were written unbound in the SVG's variable data");
}
let mut seen = HashSet::new();
let mut datasets: Vec<_> = vars
.datasets
.iter()
.filter(|ds| !ds.name.is_empty() && ds.name.trim() == ds.name && valid_xml_value(&ds.name) && seen.insert(ds.name.as_str()))
.collect();
if datasets.len() < vars.datasets.len() {
self.warn(
"Data sets whose names are blank, start or end with white space, hold characters XML can't hold or repeat a name were left out of the SVG's variable data",
);
}
if datasets.len() > MAX_DATASETS {
datasets.truncate(MAX_DATASETS);
self.warn("Data sets past the first 1,000, which Load Variable Library reads, were left out of the SVG's variable data");
}
let values = || datasets.iter().flat_map(|ds| names.iter().filter_map(|n| ds.values.get(*n)));
if values().any(|v| matches!(v, DataValue::Text(t) if !valid_xml_value(t))) {
self.warn("Characters XML can't hold were left out of data set values in the SVG's variable data");
}
let nl = if self.opts.minify { "" } else { "\n" };
let mut out = String::new();
// The XML nodes the reader makes: each start tag an element, the white space before each
// line a text node unless minified, and each paragraph's text another.
let mut nodes = 0usize;
let mut line = |depth: usize, s: &str| {
nodes += usize::from(!s.starts_with("</")) + usize::from(!self.opts.minify) + usize::from(s.starts_with("<p>") && s != "<p></p>");
out.push_str(&self.indent(depth));
out.push_str(s);
out.push_str(nl);
};
line(1, &format!("<variableSets xmlns=\"{NS_VARIABLES}\">"));
line(2, "<variableSet varSetName=\"binding1\" locked=\"none\">");
line(3, "<variables>");
for (name, element) in names.iter().zip(&refs) {
let doc_ref = element.as_ref().map_or_else(String::new, |e| format!(" docRef=\"id('{}')\"", xml_attr_escape(e)));
line(4, &format!("<variable varName=\"{name}\" trait=\"textcontent\" category=\"{NS_FLOWS}\"{doc_ref}></variable>"));
}
line(3, "</variables>");
line(3, &format!("<v:sampleDataSets xmlns:v=\"{NS_VARIABLES}\" xmlns=\"{NS_CUSTOM}\">"));
for ds in datasets {
line(4, &format!("<v:sampleDataSet dataSetName=\"{}\">", xml_attr_escape(&ds.name)));
for name in &names {
let Some(DataValue::Text(t)) = ds.values.get(*name) else { continue };
line(5, &format!("<{name}>"));
// One paragraph per line; a carriage return as a reference, which reads back as itself.
for para in t.split('\n') {
line(6, &format!("<p>{}</p>", xml_text(para).replace('\r', "&#13;")));
}
line(5, &format!("</{name}>"));
}
line(4, "</v:sampleDataSet>");
}
line(3, "</v:sampleDataSets>");
line(2, "</variableSet>");
line(1, "</variableSets>");
// Measured as written: a Latin-1 file holds other characters as references.
let mut written = std::borrow::Cow::Borrowed(out.as_str());
if self.opts.encoding == crate::Encoding::Latin1 {
super::latin1_refs(written.to_mut());
}
if written.len() > MAX_VARIABLE_DATA {
self.warn("The variable data is larger than 8 MiB, which Load Variable Library reads, and was left out of the SVG");
return String::new();
}
if nodes > MAX_NODES {
self.warn("The variable data holds more than 2,000,000 XML nodes, which Load Variable Library reads, and was left out of the SVG");
return String::new();
}
out
}
}
/// What an object a text variable names became in the file.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum Bound {
/// Not written yet.
Pending,
/// Written as an element with this id.
Written(String),
/// Written as outlines, which open as art.
Outlined,
}
+18 -1
View File
@@ -117,6 +117,17 @@ pub(crate) fn svg_filters(svg: &str, room: usize, taken: impl Fn(&str) -> bool)
/// The document SVG text `svg` holds, read with `opts`, and the warnings about what it couldn't keep.
pub(crate) fn import(svg: &str, opts: &ImportOptions, max_nodes: usize) -> Result<(Document, Vec<String>), SvgError> {
import_finding(svg, opts, max_nodes, &HashSet::new()).map(|(doc, warnings, _)| (doc, warnings))
}
/// [`import`], also finding the object made of each element whose id `wanted` lists (the first
/// object named after it), by element id.
pub(crate) fn import_finding(
svg: &str,
opts: &ImportOptions,
max_nodes: usize,
wanted: &HashSet<String>,
) -> Result<(Document, Vec<String>, crate::Found), SvgError> {
// Before anything reads the tree: the XML reader, usvg, and what is made of its tree, all by
// recursion.
let too_deep = || SvgError::TooDeep(depth::MAX_ELEMENT_DEPTH);
@@ -191,6 +202,8 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions, max_nodes: usize) -> Resul
max_nodes,
error: None,
nested_svg_images: 0,
wanted: wanted.clone(),
found: HashMap::new(),
};
im.doc.svg_scripts = found.scripts;
if im.files.nested_text() {
@@ -303,7 +316,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions, max_nodes: usize) -> Resul
}
}
im.doc.limit_depth_noting(&mut im.warnings);
Ok((im.doc, im.warnings))
Ok((im.doc, im.warnings, im.found))
}
struct Importer {
@@ -365,4 +378,8 @@ struct Importer {
error: Option<SvgError>,
/// How many nested SVG images were converted so far.
nested_svg_images: usize,
/// The element ids whose objects [`import_finding`] looks for.
wanted: HashSet<String>,
/// The object made of each element `wanted` lists, by its id.
found: HashMap<String, NodeId>,
}
+7 -1
View File
@@ -140,12 +140,16 @@ impl Importer {
Some(self.doc.alloc_id())
}
/// A new object named after element `id` (unless it has none or a made-up one).
/// A new object named after element `id` (unless it has none or a made-up one), noted as
/// found when [`Importer::wanted`] lists `id`.
fn named(&mut self, id: &str, kind: NodeKind) -> Option<Node> {
let nid = self.alloc_id()?;
let mut n = Node::new(nid, kind);
if !id.is_empty() && !made_up(id) {
n.name = Some(self.name_of(id).to_string());
if self.wanted.contains(id) {
self.found.entry(id.to_string()).or_insert(nid);
}
}
Some(n)
}
@@ -861,6 +865,8 @@ mod tests {
max_nodes: 1000,
error: None,
nested_svg_images: crate::MAX_NESTED_SVG_IMAGES,
wanted: HashSet::new(),
found: HashMap::new(),
};
let tree = usvg::Tree::from_str(r#"<svg xmlns="http://www.w3.org/2000/svg"/>"#, &usvg::Options::default()).unwrap();
let res = im.svg_image(&tree, Affine::IDENTITY);
+14
View File
@@ -246,6 +246,10 @@ pub struct ExportOptions {
/// its licence allows (whole where it forbids subsetting, left out where it forbids
/// embedding). Not with outlined text or the Tiny profile.
pub embed_fonts: bool,
/// Include Variable Data: write the document's text variables, its data sets and the object
/// each text variable is bound to as a `variableSets` element, in the layout Load Variable
/// Library reads. Opening the SVG reads them back.
pub variable_data: bool,
}
impl Default for ExportOptions {
@@ -267,6 +271,7 @@ impl Default for ExportOptions {
encoding: Encoding::Utf8,
profile: Profile::Svg11,
embed_fonts: false,
variable_data: false,
}
}
}
@@ -577,6 +582,15 @@ pub fn import_within(svg: &str, opts: &ImportOptions, max_nodes: usize) -> Resul
vectorcraft_doc::depth::with_deep_stack(|| import::import(svg, opts, max_nodes))
}
/// [`import_with`], also returning the object made of each element whose id `ids` lists (the
/// first object named after it), by element id. Ids with no object are left out.
pub fn import_finding(svg: &str, opts: &ImportOptions, ids: &std::collections::HashSet<String>) -> Result<(Document, Vec<String>, Found), SvgError> {
vectorcraft_doc::depth::with_deep_stack(|| import::import_finding(svg, opts, MAX_NODES, ids))
}
/// The object made of each element [`import_finding`] looked for, by element id.
pub type Found = std::collections::HashMap<String, vectorcraft_doc::NodeId>;
/// Reads a linked file: its bytes and the link to it (absolute path, size, time and hash), or
/// `None` when there is no readable file at the (absolute) path.
pub type ReadFile<'a> = &'a (dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)> + Sync);
+21
View File
@@ -227,6 +227,9 @@ pub(super) fn option_fields(ui: &mut egui::Ui, d: &mut Dialog, screens: bool) {
check(ui, d, "embedFonts", tl!("Embed Fonts (Glyphs Used)"), !d.bool("outlineText") && !tiny);
check(ui, d, "fewerTspans", tl!("Fewer <tspan> Elements"), !d.bool("outlineText"));
check(ui, d, "metadata", tl!("Include Metadata"), true);
if !screens {
check(ui, d, "variableData", tl!("Include Variable Data"), true);
}
}
/// Use Artboards: all of them or a range; off = the bounds of all art.
@@ -443,6 +446,24 @@ mod tests {
assert_eq!((written.borrow().len(), written.borrow()[1].0.as_str()), (2, "/tmp/doc.svg"));
}
/// Include Variable Data starts off, draws in the dialog, and writes the text variables and
/// data sets into the saved SVG when it is on.
#[test]
fn include_variable_data_reaches_the_file() {
let (mut app, written) = app("/tmp/vars.svg");
let id = app.run("text.create", json!({"x": 10, "y": 60, "text": "Alice"})).unwrap()["id"].clone();
app.run("variable.define", json!({"name": "Name", "kind": "text"})).unwrap();
app.run("variable.bind", json!({"variable": "Name", "ids": [id]})).unwrap();
app.run("dataset.new", json!({"name": "Row", "values": {"Name": "Bob"}})).unwrap();
app.run("file.saveAs", Value::Null).unwrap();
assert!(!app.ui.dialog.as_ref().unwrap().bool("variableData"), "off at first");
frame(&mut app);
app.ui.dialog.as_mut().unwrap().fields.insert("variableData".into(), json!(true));
super::super::confirm(&mut app).unwrap();
let svg = text(&written, 0);
assert!(svg.contains("<variable varName=\"Name\"") && svg.contains("dataSetName=\"Row\"") && svg.contains("<p>Bob</p>"), "{svg}");
}
#[test]
fn profile_encoding_and_embedded_fonts_reach_the_file() {
let (mut app, written) = app("/tmp/tiny.svg");
+1
View File
@@ -1634,6 +1634,7 @@ font-technology Variable Променливи
Include Hidden Layers Включване на скритите слоеве
Include Linked Files Включване на свързаните файлове
Include Metadata Включване на метаданни
Include Variable Data Включване на данни за променливи
Include non-printing layers Включване на слоеве, които не се печат
Write pages longer than 200 in at full size in points Записване на страници, по-дълги от 200 in, в пълен размер в точки
Include SVG Code Включване на SVG код
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3091,6 +3091,7 @@ antiAlias Sharp Oštro
Hide Code Sakrij kod
Images: Slike:
Include Metadata Uključi metapodatke
Include Variable Data Uključi podatke varijabli
Internal CSS Interni CSS
Layer Names Nazivi slojeva
Minify Minificiraj
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2110,6 +2110,7 @@ digits Malayalam Malaiàlam
Include Hidden Layers Inclou les capes ocultes
Include Linked Files Inclou els fitxers enllaçats
Include Metadata Inclou les metadades
Include Variable Data Inclou les dades de variables
Include non-printing layers Inclou les capes no imprimibles
Write pages longer than 200 in at full size in points Escriu les pàgines de més de 200 in a mida real, en punts
Include Objects On Canvas Inclou els objectes del llenç
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3127,6 +3127,7 @@ antiAlias Sharp Ostré
Hide Code Skrýt kód
Images: Obrázky:
Include Metadata Zahrnout metadata
Include Variable Data Zahrnout data proměnných
Internal CSS Interní CSS
Layer Names Názvy vrstev
Minify Minimalizovat
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2911,6 +2911,7 @@ antiAlias Sharp Skarp
Hide Code Skjul kode
Images: Billeder:
Include Metadata Medtag metadata
Include Variable Data Medtag variabeldata
Internal CSS Intern CSS
Layer Names Lagnavne
Minify Minificér
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1693,6 +1693,7 @@ file-filter Templates Vorlagen
Include Hidden Layers Ausgeblendete Ebenen einschließen
Include Linked Files Verknüpfte Dateien einschließen
Include Metadata Metadaten einschließen
Include Variable Data Variablendaten einschließen
Include non-printing layers Nicht druckende Ebenen einschließen
Write pages longer than 200 in at full size in points Seiten über 200 in in voller Größe in Punkt schreiben
Include SVG Code SVG-Code einschließen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1608,6 +1608,7 @@ font-technology Variable Μεταβλητές
Include Hidden Layers Συμπερίληψη κρυφών επιπέδων
Include Linked Files Συμπερίληψη συνδεδεμένων αρχείων
Include Metadata Συμπερίληψη μεταδεδομένων
Include Variable Data Συμπερίληψη δεδομένων μεταβλητών
Include non-printing layers Συμπερίληψη μη εκτυπώσιμων επιπέδων
Write pages longer than 200 in at full size in points Εγγραφή σελίδων μεγαλύτερων από 200 in σε πλήρες μέγεθος, σε στιγμές
Include SVG Code Συμπερίληψη κώδικα SVG
Can't render this file because it is too large.
+1
View File
@@ -1689,6 +1689,7 @@ font-technology Variable Variables
Include Hidden Layers Incluir capas ocultas
Include Linked Files Incluir archivos vinculados
Include Metadata Incluir metadatos
Include Variable Data Incluir datos de variables
Include non-printing layers Incluir capas no imprimibles
Write pages longer than 200 in at full size in points Escribir las páginas de más de 200 in a tamaño real, en puntos
Include SVG Code Incluir código SVG
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3071,6 +3071,7 @@ antiAlias Sharp Terävä
Hide Code Piilota koodi
Images: Kuvat:
Include Metadata Sisällytä metatiedot
Include Variable Data Sisällytä muuttujatiedot
Internal CSS Sisäinen CSS
Layer Names Tasojen nimet
Minify Pienennä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1683,6 +1683,7 @@ font-technology Variable Variables
Include Hidden Layers Inclure les calques masqués
Include Linked Files Inclure les fichiers liés
Include Metadata Inclure les métadonnées
Include Variable Data Inclure les données des variables
Include non-printing layers Inclure les calques non imprimables
Write pages longer than 200 in at full size in points Écrire les pages de plus de 200 in en taille réelle, en points
Include SVG Code Inclure le code SVG
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2328,6 +2328,7 @@ antiAlias Sharp शार्प
Include in playback प्लेबैक में शामिल करें
Include Linked Files लिंक्ड फ़ाइलें शामिल करें
Include Metadata मेटाडाटा शामिल
Include Variable Data वेरिएबल डेटा शामिल
Include non-printing layers गैर-प्रिंटिंग लेयर्स को शामिल करें
Write pages longer than 200 in at full size in points 200 in से लंबे पेज पूरे आकार में, पॉइंट में लिखें
Include Objects On Canvas कैनवास पर ऑब्जेक्ट्स शामिल करें
Can't render this file because it is too large.
+1
View File
@@ -2106,6 +2106,7 @@ file-filter Templates Sablonok
Include Hidden Layers Rejtett rétegek hozzáadása
Include Linked Files Csatolt fájlok hozzáadása
Include Metadata Metaadatok hozzáadása
Include Variable Data Változóadatok hozzáadása
Include non-printing layers Nem nyomtatandó rétegek hozzáadása
Write pages longer than 200 in at full size in points 200 in-nél hosszabb oldalak írása teljes méretben, pontban
Include SVG Code SVG-kód hozzáadása
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2911,6 +2911,7 @@ antiAlias Sharp Tajam
Hide Code Sembunyikan kode
Images: Gambar:
Include Metadata Sertakan metadata
Include Variable Data Sertakan data variabel
Internal CSS CSS internal
Layer Names Nama lapisan
Minify Minify
Can't render this file because it contains an unexpected character in line 2403 and column 5.
+1
View File
@@ -1724,6 +1724,7 @@ file-filter Templates Modelli
Include Hidden Layers Includi livelli nascosti
Include Linked Files Includi file collegati
Include Metadata Includi metadati
Include Variable Data Includi dati delle variabili
Include non-printing layers Includi livelli non stampabili
Write pages longer than 200 in at full size in points Scrivi le pagine più lunghe di 200 in a grandezza reale, in punti
Include SVG Code Includi codice SVG
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2065,6 +2065,7 @@ antiAlias Sharp シャープ
Include Hidden Layers 非表示レイヤーを含む
Include Linked Files リンクファイルを含める
Include Metadata メタデータを含む
Include Variable Data 変数データを含む
Include SVG Code SVG コードを含める
Include Swatches for Tints ティント用のスウォッチを含める
Include non-printing layers 印刷しないレイヤーを含む
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2753,6 +2753,7 @@ antiAlias Sharp მკვეთრი
Hide Code კოდის დამალვა
Images: გამოსახულებები:
Include Metadata მეტამონაცემების ჩართვა
Include Variable Data ცვლადების მონაცემების ჩართვა
Internal CSS შიდა CSS
Layer Names შრეების სახელები
Minify მინიფიკაცია
Can't render this file because it is too large.
+1
View File
@@ -2094,6 +2094,7 @@ antiAlias Sharp 날카롭게
Include Hidden Layers 숨겨진 레이어 포함
Include Linked Files 링크된 파일 포함
Include Metadata 메타데이터 포함
Include Variable Data 변수 데이터 포함
Include SVG Code SVG 코드 포함
Include Swatches for Tints 농도별 견본 포함
Include non-printing layers 인쇄되지 않는 레이어 포함
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2925,6 +2925,7 @@ antiAlias Sharp Scherp
Hide Code Code verbergen
Images: Afbeeldingen:
Include Metadata Metadata opnemen
Include Variable Data Variabelegegevens opnemen
Internal CSS Interne CSS
Layer Names Laagnamen
Minify Minify
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3012,6 +3012,7 @@ antiAlias Sharp Ostre
Hide Code Ukryj kod
Images: Obrazy:
Include Metadata Dołącz metadane
Include Variable Data Dołącz dane zmiennych
Internal CSS Wewnętrzny CSS
Layer Names Nazwy warstw
Minify Minimalizuj
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1484,6 +1484,7 @@ antiAlias Sharp Nítido
Include Hidden Layers Incluir Camadas Ocultas
Include Linked Files Incluir Arquivos Vinculados
Include Metadata Incluir Metadados
Include Variable Data Incluir Dados de Variáveis
Include SVG Code Incluir Código SVG
Include Swatches for Tints Incluir Amostras para Tons
Include non-printing layers Incluir camadas de não impressão
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1713,6 +1713,7 @@ file-filter Templates Шаблоны
Include Hidden Layers Включать скрытые слои
Include Linked Files Включать связанные файлы
Include Metadata Включать метаданные
Include Variable Data Включать данные переменных
Include non-printing layers Включать непечатаемые слои
Write pages longer than 200 in at full size in points Записывать страницы длиннее 200 in в полном размере, в пунктах
Include SVG Code Включать код SVG
Can't render this file because it is too large.
+1
View File
@@ -1647,6 +1647,7 @@ font-technology Variable Variabla
Include Hidden Layers Inkludera dolda lager
Include Linked Files Inkludera länkade filer
Include Metadata Inkludera metadata
Include Variable Data Inkludera variabeldata
Include non-printing layers Inkludera lager som inte skrivs ut
Write pages longer than 200 in at full size in points Skriv sidor längre än 200 in i full storlek i punkter
Include SVG Code Inkludera SVG-kod
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1527,6 +1527,7 @@ font-technology Variable Değişken
Include Hidden Layers Gizli Katmanları Ekle
Include Linked Files Bağlantılı Dosyaları Ekle
Include Metadata Meta Verileri Ekle
Include Variable Data Değişken Verilerini Ekle
Include non-printing layers Yazdırılmayan katmanları dahil et
Write pages longer than 200 in at full size in points 200 in'den uzun sayfaları tam boyutta, punto olarak yaz
Include SVG Code SVG Kodunu Ekle
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1718,6 +1718,7 @@ file-filter Templates Шаблони
Include Hidden Layers Включати приховані шари
Include Linked Files Включати пов’язані файли
Include Metadata Включати метадані
Include Variable Data Включати дані змінних
Include non-printing layers Включати недруковані шари
Write pages longer than 200 in at full size in points Записувати сторінки, довші за 200 in, у повному розмірі, у пунктах
Include SVG Code Вмикати код SVG
Can't render this file because it is too large.
+1
View File
@@ -1995,6 +1995,7 @@ digits Hindi Hindi
Include Hidden Layers Bao gồm các lớp ẩn
Include Linked Files Bao gồm tệp được liên kết
Include Metadata Bao gồm siêu dữ liệu
Include Variable Data Bao gồm dữ liệu biến
Include non-printing layers Bao gồm các lớp không in
Write pages longer than 200 in at full size in points Ghi các trang dài hơn 200 in ở kích thước đầy đủ, theo điểm
Include SVG Code Bao gồm mã SVG
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -1643,6 +1643,7 @@ file-filter Templates 模板
Include Hidden Layers 包含隐藏图层
Include Linked Files 包含链接文件
Include Metadata 包含元数据
Include Variable Data 包含变量数据
Include non-printing layers 包含非打印图层
Write pages longer than 200 in at full size in points 以实际尺寸(点)写入长于 200 in 的页面
Include SVG Code 包含 SVG 代码
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1689,6 +1689,7 @@ file-filter Templates 範本
Include Hidden Layers 包含隱藏圖層
Include Linked Files 包含連結檔案
Include Metadata 包含中繼資料
Include Variable Data 包含變數資料
Include non-printing layers 包含不列印的圖層
Write pages longer than 200 in at full size in points 以實際尺寸(點)寫入長於 200 in 的頁面
Include SVG Code 包含 SVG 程式碼
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -268,7 +268,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
| PDF | open, place, save | yes (authored ActualText in editable import, #6165; layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160); an image mask (`/ImageMask true`) painted in a solid color opens as a grayscale image colorized by that color, a spot color keeping its swatch and tint, so Fill and Recolor Artwork reach it, and the same mask painted again in another color keeps that color (#6313); an image in a Separation space opens as a grayscale image of its tints colorized by its ink at 100 % (a spot swatch, a process plate or Registration), its soft mask as its opacity mask (#6314); a DeviceN image of one colorant opens the same way, and one of process plates only (the ink `None` may be among them) opens in CMYK with each colorant's tint in its plate (#6348) PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. |
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CIDFonts the file doesn't carry not drawn, but for those over a predefined CMap (`UniJIS-UCS2-H`, `90ms-RKSJ-H`, `GBK-EUC-H`, `KSCms-UHC-H`, `ETen-B5-H`), set as type in the font of the CIDFont's name, vertical over a `-V` CMap; a name that ends in the row-and-cell CMap `H` or `V` splits off the CMap only when the file defines the CIDFont of that name, or the name has a hyphen and an installed face that covers Japanese has it; name-keyed fonts in a FontSet set as type by name; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`, and the moves of `kshow`'s procedure, run between each two characters) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow`, `xyshow` or a `kshow` procedure can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and an embedded Type 1 descendant's from its charstrings, and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`; `bfchar` and `bfrange` mappings give a base-font descendant its codes or glyph names) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry, and the CID-keyed fonts of a FontSet's Compact Font Format data (Type 2 charstrings); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`); the regions of exported masks and filters (opacity masks, inverted masks, knockout groups) cover the art they apply to, and over type keep a region that covers any artwork (#6401, `crates/svg/tests/it/blend_export.rs`) |
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`); the regions of exported masks and filters (opacity masks, inverted masks, knockout groups) cover the art they apply to, and over type keep a region that covers any artwork (#6401, `crates/svg/tests/it/blend_export.rs`); SVG Options › Include Variable Data writes the text variables, data sets and bound type objects, and opening the SVG reads them back (`tests::variables::an_svg_with_variable_data_opens_with_its_variables_data_sets_and_bindings`) |
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). DXF Options' Preset menu and `document.exportDxf`'s `dxfPreset: "cnc"` set the CNC / CAM options (#5500); VCarve QA remains open |
| HP-GL/2 `.plt`, `.hpgl`, `.hgl`, `.hpg` | — | yes (pen moves, pens and widths, line types and attributes, circles, arcs, wedges and Bézier curves, filled and edged rectangles, wedges and polygons, encoded polylines, scaling and rotation; labels, symbol mode, raster images and hatching aren't drawn) | yes (`.plt`: each visible path once along its line, a pen per color or a pen table, type as outlines, clipping applied, HP-GL or HP-GL/2 with pen colors and encoded polylines) | recognized by its first instructions after any printer escapes, under its own extensions or one no other format reads; `crates/cad/src/hpgl/tests.rs`, `crates/cad/src/hpgl/write/tests.rs` (plots read back by the reader), `tests_hpgl.rs`, `import_fuzz.rs` |
| DWG | open, export | no | no | no open spec; DXF instead |
+14 -1
View File
@@ -823,7 +823,20 @@ lists every option with its default. `encoding` is `utf8`, `utf16` (big-endian a
a warning). `embedFonts: true` embeds the fonts type uses as `@font-face` rules subset to the characters used; a
font whose licence (OS/2 `fsType`) forbids subsetting is embedded whole, one that forbids embedding is left out
with a warning. Type names its faces by numeric `font-weight` (600 for Semibold and for a Japanese face's W6, 300 for Light, `bold` for 700),
embedded or not. `svgz` takes the same options and writes the SVG gzipped (`.svgz` files also open,
embedded or not. `variableData: true` (SVG Options › Include Variable Data) writes the document's text variables and
data sets as a `variableSets` element in the layout `variable.loadLibrary` reads, so the SVG works as a data merge
template ([help](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/merge-data.html)).
Each text variable names its bound type object by the element's id, `docRef="id('…')"` (VectorCraft's own form); an
unnamed object, or any object with `objectIds: "minimal"`, is given an id for it. A variable bound to several type
objects names the first by object id. Variables of other kinds, text variables whose names aren't XML names, and data
sets whose names are blank, have white space at either end, hold characters XML can't hold or repeat a name are left
out, each with a warning. Only the first 256 text variables and the first 1,000 data sets are written, and variable
data larger than 8 MiB or holding more than 2,000,000 XML nodes is left out (the limits `variable.loadLibrary`
reads), each with a warning. With `outlineText`, the bound type is written as outlines, so its text variables open
unbound (with a warning). `document.open` reads the variable data of an SVG back (unless the SVG reopens from
Preserve Editing): its text variables and data sets, no active data set, and each text variable bound to the type
object its `docRef` names; the art stays as the file draws it. Variable data that `variable.loadLibrary` would refuse
is left out with the warning "the file's variable data wasn't read", and the art opens. `svgz` takes the same options and writes the SVG gzipped (`.svgz` files also open,
place and paste). `run_command document.save {path: "x.svg", svg: {…}}` saves as SVG (or `.svgz`).
Colorized grayscale and 1-bit images export in their fill color; linked mode writes a coverage PNG, and Tiny 1.2 writes a colorized PNG.