mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 10:45:45 +00:00
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:
File diff suppressed because one or more lines are too long
@@ -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) →
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', " ")));
|
||||
}
|
||||
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,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user