Merge main into fix/plugin-stale-grant

This commit is contained in:
dotCooCoo
2026-11-08 21:00:56 -08:00
80 changed files with 2108 additions and 305 deletions
+5 -4
View File
@@ -93,16 +93,17 @@ pub use save::{
remembered_save_options, save_filters, save_format, save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder,
templates_folder,
};
pub(crate) use screens::{
PARAM_KEYS as SCREEN_PARAM_KEYS, ROW_KEYS as SCREEN_ROW_KEYS, UNWRITTEN_FORMATS as SCREEN_UNWRITTEN_FORMATS,
check_settings as check_screen_settings, export as export_screens, safe_file_name, store_settings as store_screen_settings,
with_export_preset as with_screen_export_preset,
};
pub use screens::{
PRESET_FILE_EXTS as SCREEN_EXPORT_PRESET_EXTS, PRESET_FILE_FORMAT as SCREEN_EXPORT_PRESET_FORMAT, PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS,
PRESETS as SCREEN_PRESETS, SHARED_KEYS as SCREEN_SETTINGS_KEYS, ScreenExport, ScreenExportPreset, ScreenSize,
builtin_presets as screen_export_builtin_presets, first_file as screen_first_file, preset_list as screen_export_preset_list,
preset_rows as screen_preset_rows,
};
pub(crate) use screens::{
check_settings as check_screen_settings, export as export_screens, safe_file_name, store_settings as store_screen_settings,
with_export_preset as with_screen_export_preset,
};
pub use svg::options_map as svg_options;
/// Atomic file writes (a temporary file renamed over the target: a failed write never damages the
/// file it replaces), for the apps' own writers too.
File diff suppressed because one or more lines are too long
+13 -10
View File
@@ -184,7 +184,8 @@ fn through_editing_data(
) -> (Document, Vec<String>, bool) {
let Some(layers) = layers else { return (doc, warnings, false) };
let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines;
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit);
let (doc, mut warnings, read) =
vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit, opts.legacy_names);
// The layers weren't read (or couldn't be): the notes of reading the PDF part still hold.
if !read {
return (doc, warnings, false);
@@ -209,12 +210,12 @@ fn from_editing_data_alone(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions)
return None;
}
let private = opened.illustrator_data()?;
Some(editing_copy_alone(&private))
Some(editing_copy_alone(&private, opts))
}
/// [`from_editing_data_alone`] of `private`, the editor's copy already read.
fn editing_copy_alone(private: &[u8]) -> Result<(Document, Vec<String>)> {
vectorcraft_eps::ai_alone(private)
fn editing_copy_alone(private: &[u8], opts: &LoadOptions) -> Result<(Document, Vec<String>)> {
vectorcraft_eps::ai_alone_with(private, opts.legacy_names)
.map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})")))
}
@@ -247,7 +248,7 @@ fn without_pdf_part(bytes: &[u8], opts: &LoadOptions, e: &EngineError) -> Option
return None;
}
let private = vectorcraft_pdf::illustrator_data_without_pdf_part(bytes, opts.password.as_deref())?;
let (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit).ok()?;
let (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit, opts.legacy_names).ok()?;
let notes = std::iter::once(format!("the PDF part couldn't be read ({e}): the file opened from its editing data")).chain(warnings).collect();
Some(outlined_if_asked(doc, notes, opts))
}
@@ -323,7 +324,7 @@ pub(crate) fn page_or_editing_data(
match read {
Ok((doc, notes)) => Ok((doc, notes, false)),
Err(e) if is_placeholder(&e) => {
let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), ..Default::default() })?;
let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), legacy_names: opts.legacy_names, ..Default::default() })?;
Ok((l.doc, l.warnings, true))
}
Err(e) if page_index == 0 => match load_with(name, bytes, &LoadOptions { pages: None, ..opts.clone() }) {
@@ -352,7 +353,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
_ if matches!(format.id, "eps" | "ps") || vectorcraft_pdf::is_postscript(bytes) => {
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
restore_or_import(editing, || {
let r = vectorcraft_eps::import_trusting(bytes, opts.editing_data, opts.trust_editing_data)
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
.map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
Ok((r.document, r.warnings))
})?
@@ -391,7 +392,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
if let Some((o, private)) = opened.as_ref().ok().filter(|_| reads_layers_alone(opts)).and_then(|o| Some((o, editing_copy(o, opts)?)))
{
let pages = o.trim_pages();
match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit) {
match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit, opts.legacy_names) {
Ok((mut doc, warnings)) => {
doc.setup.bleed = pages.bleed;
editing_data = true;
@@ -413,8 +414,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
// decoded draws nothing either): its notes are at most the page's own.
let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w));
let copy_alone = || match &private {
Some(private) => Some(editing_copy_alone(private)),
None if layers.is_some() => opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p)),
Some(private) => Some(editing_copy_alone(private, opts)),
None if layers.is_some() => {
opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p, opts))
}
None => None,
};
let alone = (r.only_text || empty)
+15 -3
View File
@@ -39,6 +39,9 @@ pub struct LoadOptions {
/// A `.ai` opens from its editing copy even where that draws differently from its PDF part
/// (which can be older than it), with a warning, instead of from the PDF part.
pub trust_editing_data: bool,
/// The encoding names in a `.ai` or EPS's editing data are read in when they aren't UTF-8
/// (`legacyNameEncoding`).
pub legacy_names: vectorcraft_eps::NameEncoding,
/// A PDF or `.ai` saved with Preserve Editing opens from the VectorCraft document it carries;
/// off, it opens as another app would open it, from the editing copy of its art or its pages.
pub native: bool,
@@ -57,6 +60,7 @@ impl Default for LoadOptions {
dxf: Default::default(),
editing_data: true,
trust_editing_data: false,
legacy_names: vectorcraft_eps::NameEncoding::default(),
native: true,
}
}
@@ -65,8 +69,9 @@ impl Default for LoadOptions {
impl LoadOptions {
/// From command params: `pages` (`"2-3, 5"`, a page number or `"all"`), `page` (one page, when
/// `pages` isn't given), `cropTo` (or `crop`), `password`, `colorMode` (`rgb` | `cmyk`) with `grays`,
/// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`) and `dxf`
/// (the DXF import options).
/// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`),
/// `legacyNameEncoding` (one of [`vectorcraft_eps::NameEncoding::LABELS`]) and `dxf` (the DXF
/// import options).
pub fn from_params(cmd: &str, p: &Value) -> Result<Self> {
let pages = match p.get("pages").filter(|v| !v.is_null()).or_else(|| p.get("page").filter(|v| !v.is_null())) {
None => None,
@@ -95,8 +100,15 @@ impl LoadOptions {
Some(Value::String(s)) if s.eq_ignore_ascii_case("always") => (true, true),
Some(v) => (v.as_bool().ok_or_else(|| bad(cmd, "editingData must be true, false or \"always\""))?, false),
};
let legacy_names = match p.get("legacyNameEncoding").filter(|v| !v.is_null()) {
None => vectorcraft_eps::NameEncoding::default(),
Some(v) => v
.as_str()
.and_then(vectorcraft_eps::NameEncoding::from_label)
.ok_or_else(|| bad(cmd, format!("legacyNameEncoding must be one of {}", vectorcraft_eps::NameEncoding::LABELS.join(", "))))?,
};
let dxf = super::dxfimport::options(cmd, p.get("dxf"))?;
Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, native: true })
Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, legacy_names, native: true })
}
/// Does the document read only part of the file, or read it differently (a page range, another
+1 -1
View File
@@ -1102,7 +1102,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Save Document",
[],
None,
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified",
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused",
has_doc,
|s, p| save(s, SaveMode::Save, p)
),
+35 -2
View File
@@ -193,7 +193,10 @@ pub(super) fn name_without_device(name: &str) -> String {
}
/// Keys of a format row that aren't encoder options.
const ROW_KEYS: [&str; 7] = ["format", "scale", "ppi", "width", "height", "suffix", "folder"];
pub(crate) const ROW_KEYS: [&str; 7] = ["format", "scale", "ppi", "width", "height", "suffix", "folder"];
/// The writable formats a format row can't name.
pub(crate) const UNWRITTEN_FORMATS: [&str; 5] = ["template", "txt", "dxf", "emf", "wmf"];
/// One format row: what to write, at which size, with which suffix, into which sub-folder.
struct Row {
@@ -214,7 +217,7 @@ impl Row {
return Err(bad(C, "each format is an object {format, scale?, suffix?}"));
}
let format = writable(C, Some(str_param(row, "format").unwrap_or("png")), None)?;
if matches!(format.id, "template" | "txt" | "dxf" | "emf" | "wmf") {
if UNWRITTEN_FORMATS.contains(&format.id) {
return Err(bad(C, "Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft"));
}
// Vector formats have no pixel size: it doesn't apply and adds no @2x suffix (not even one
@@ -285,6 +288,36 @@ impl Row {
/// or where to.
pub const SHARED_KEYS: [&str; 6] = ["formats", "preset", "settings", "prefix", "subfolders", "antiAlias"];
/// The keys `document.exportForScreens` reads: those of [`ScreenParams`], the formats and their
/// settings, `antiAlias`, `exportPreset` and `openLocation`, which the app reads.
pub(crate) const PARAM_KEYS: [&str; 25] = [
"artboard",
"artboards",
"range",
"folder",
"zip",
"fullDocument",
"includeBleed",
"includeArtboardFill",
"subfolders",
"preset",
"prefix",
"assets",
"layers",
"selection",
"name",
"counterStart",
"counterPer",
"date",
"folders",
"pdfs",
"formats",
"settings",
"antiAlias",
"exportPreset",
"openLocation",
];
/// What `document.exportForScreens` reads besides the formats.
#[derive(Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
@@ -1022,6 +1022,21 @@ fn an_ai_file_saved_without_its_pdf_part_pastes_from_its_editing_data() {
assert!(e.contains("placeholder"), "{e}");
}
/// `file.place` reads a `.ai` saved without its PDF part in the `legacyNameEncoding` it is given.
#[test]
fn an_ai_file_saved_without_its_pdf_part_places_in_the_encoding_picked() {
let mut data = ai::sample_data();
let at = data.windows(4).position(|w| w == b"Pair").unwrap();
// 빨강 in code page 949.
data.splice(at..at + 4, br"\273\241\260\255".iter().copied());
let file = vectorcraft_format::base64_encode(&ai::ai(&compressed(&data), PLACEHOLDER));
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("file.place", &json!({"name": "art.ai", "dataBase64": file, "link": false, "legacyNameEncoding": "windows-949"})).unwrap();
let placed = s.doc().unwrap().doc.layers[0].children().unwrap()[0].clone();
assert_eq!(names(&placed.children().unwrap()[1..]), ["빨강", "Ring", "Window"], "{r}");
}
#[test]
fn an_ai_file_saved_without_its_pdf_part_places_from_its_editing_data() {
let [sample, damaged, plain] = placeholder_files();
@@ -184,6 +184,41 @@ showpage
assert!(e.contains("junk.eps"), "{e}");
}
/// `legacyNameEncoding` picks the code page of a legacy `.ai`'s layer names that aren't UTF-8; by
/// default they come in as Windows-1252, with a warning naming them. An encoding it doesn't know is
/// refused.
#[test]
fn legacy_ai_layer_names_open_in_the_encoding_picked() {
let mut s = Session::new();
// A layer named 레이어 1 in Windows code page 949.
let ai = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 100 100\n%%DocumentNeededResources: procset Adobe_IllustratorA_AI3 1.0 0\n%AI3_TemplateBox: 50 50 50 50\n%%EndComments\n\
%%BeginProlog\n%%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 0\n%%EndProlog\n%%BeginSetup\nAdobe_IllustratorA_AI3 /initialize get exec\n%%EndSetup\n\
%AI5_BeginLayer\n1 1 1 1 0 0 2 0 0 0 Lb\n(\\267\\271\\300\\314\\276\\356 1) Ln\n0 0 0 1 K\n0 i 1 w\n10 10 m\n90 90 L\nS\nLB\n%AI5_EndLayer--\n%%PageTrailer\n%%Trailer\n%%EOF\n";
let open_as = |s: &mut Session, encoding: Option<&str>| {
let mut p = json!({"name": "korean.ai", "dataBase64": vectorcraft_format::base64_encode(ai)});
if let Some(e) = encoding {
p["legacyNameEncoding"] = json!(e);
}
s.execute("document.open", &p)
};
let r = open_as(&mut s, None).unwrap();
assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("·¹À̾î 1"));
assert_eq!(
r["warnings"],
json!(["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"])
);
for encoding in ["windows-949", "EUC-KR"] {
let r = open_as(&mut s, Some(encoding)).unwrap();
assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("레이어 1"), "{encoding}");
assert!(r["warnings"][0].as_str().is_some_and(|w| w.contains("`레이어 1`") && w.contains("windows-949")), "{r}");
}
let e = open_as(&mut s, Some("utf-16")).unwrap_err();
assert!(
matches!(&e, EngineError::BadParams { msg, .. } if msg == "legacyNameEncoding must be one of windows-1252, macintosh, windows-949, shift_jis, gbk, big5"),
"{e}"
);
}
#[test]
fn eps_files_place_as_one_group_clipped_to_their_bounding_box() {
let mut s = rich();
+178 -34
View File
@@ -6,9 +6,9 @@
//! by `|`; a `?` marks an optional one, and a `:`, `=` or `(` starts what the entry says about it. An entry that starts
//! with `…` stands for keys listed elsewhere: those of each command it names after `as` or `of`
//! (`…settings as file.new`), or, naming none, any key (the options of the format an export picks). Objects inside a value aren't
//! described key by key, so only the top level is checked, except in an export's options: those
//! are checked against the options of the formats the command can write, in `marks`, `bleed` and
//! the other objects too ([`check_export_options`]).
//! described key by key, so only the top level is checked, except in the options of an export, a
//! save or Export for Screens: those are checked against the options of the formats the command
//! can write, in `marks`, `bleed` and the other objects too ([`check_export_options`]).
//!
//! [`Session::execute`] answers a key the description doesn't name with a parameter error
//! ([`check_params`]), for every caller (the app, the control channel, MCP, the CLI) and for the
@@ -531,25 +531,183 @@ fn check_option_tree(id: &str, within: &str, given: &serde_json::Map<String, Val
Ok(())
}
/// The options of an export command `id`. `document.export`, `document.exportSelection` and
/// `document.serialize` take a key that some format reads (one object can carry several formats'
/// options; `document.formats` lists them). `document.exportPdf`, `document.pdfSettings` and
/// `pdf.preset.save` take a key that PDF reads, `document.exportEps` one that EPS reads and
/// `document.exportDxf` one that DXF reads. Where a command takes `marks`, `bleed`, `flattener` or
/// another object option of PDF or EPS, only the object's own keys pass. An SVG export's `svg`
/// object is checked when it is read. Saved presets are not checked: one with a key that is no
/// longer read keeps loading.
/// `given`, the options object at `within` (`""` for the params themselves, `options.`,
/// `formats[0].`), has only `keys`, and only their own keys in the PDF and EPS object options
/// (`trees`). `why_not` says why a key that is not near any of `keys` is refused, and where the
/// keys are listed.
fn check_option_keys(id: &str, within: &str, given: &serde_json::Map<String, Value>, keys: &[&str], why_not: &str, trees: &Value) -> Result<()> {
for (key, value) in given {
if !keys.contains(&key.as_str()) {
return Err(bad(
id,
match nearest(key, keys) {
Some(near) => format!("unknown parameter `{within}{key}`; did you mean `{within}{near}`?"),
None => format!("unknown parameter `{within}{key}`; {why_not}"),
},
));
}
if let (Some(Value::Object(schema)), Value::Object(value)) = (trees.get(key), value) {
check_option_tree(id, &format!("{within}{key}."), value, schema)?;
}
}
Ok(())
}
/// Why an export option that no format reads is refused.
const NO_FORMAT: &str = "no format takes it (see document.formats)";
/// Why a save option that no save format reads is refused.
const NO_SAVE_FORMAT: &str = "no save format takes it (see file.formatOptions)";
/// The format ids Export for Screens writes: the writable formats, those exporter plug-ins add
/// included, but those it doesn't write.
pub fn screen_formats() -> Vec<&'static str> {
use super::fileio::{FORMATS, SCREEN_UNWRITTEN_FORMATS, plugin_formats};
let added = plugin_formats();
FORMATS.iter().chain(added.iter().copied()).filter(|f| f.write && !SCREEN_UNWRITTEN_FORMATS.contains(&f.id)).map(|f| f.id).collect()
}
/// The keys a format row of Export for Screens, and the settings of a format, take: the row's own
/// (`format`, `scale`…), `includeArtboardFill` and `richBlack`, which the encoders read from a
/// row's options (`with_artboard_fills`, the render options), `params`, an exporter plug-in's own,
/// and a key some format reads, as `document.export` takes them.
fn screen_option_keys() -> Vec<&'static str> {
use super::fileio::{FORMATS, SCREEN_ROW_KEYS};
let mut keys: Vec<&str> = SCREEN_ROW_KEYS.to_vec();
keys.extend(["includeArtboardFill", "richBlack", "params"]);
keys.extend(FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).map(|o| o.name));
keys
}
/// A format row of Export for Screens, or the settings of a format, without the keys the export
/// refuses in it ([`check_screen_rows`]), inside `marks`, `svg` and the other object options too:
/// a saved row or preset that holds a key that is no longer read loads without it.
pub fn drop_unread_row_options(row: &mut serde_json::Map<String, Value>) {
fn retain_tree(o: &mut serde_json::Map<String, Value>, schema: &serde_json::Map<String, Value>) {
o.retain(|k, _| schema.contains_key(k));
for (k, v) in o.iter_mut() {
if let (Some(Value::Object(inner)), Value::Object(v)) = (schema.get(k), v) {
retain_tree(v, inner);
}
}
}
let keys = screen_option_keys();
row.retain(|k, _| keys.contains(&k.as_str()));
let trees = screen_option_trees();
for (k, v) in row.iter_mut() {
if let (Some(Value::Object(schema)), Value::Object(v)) = (trees.get(k), v) {
retain_tree(v, schema);
}
}
}
/// [`option_trees`] of Export for Screens: its remembered settings mark PDF security whose
/// passwords were left out with `security.passwordRequired`, which the export reads.
fn screen_option_trees() -> Value {
let mut trees = option_trees();
if let Some(security) = trees.get_mut("security").and_then(Value::as_object_mut) {
security.insert("passwordRequired".into(), Value::Null);
}
trees
}
/// The object options an export or save checks key by key: PDF's `marks`, `bleed` and the others,
/// the flattener's, and SVG's `svg` with the artboard keys it may carry.
fn option_trees() -> Value {
let mut trees = pdf_option_tree();
if let Some(tree) = trees.as_object_mut() {
tree.insert("flattener".into(), serde_json::to_value(super::flatten::FlattenOptions::default()).unwrap_or_default());
let mut svg = serde_json::to_value(vectorcraft_svg::ExportOptions::default()).unwrap_or_default();
if let Some(svg) = svg.as_object_mut() {
svg.extend(super::fileio::ARTBOARD_PARAMS.iter().chain(&["useArtboards"]).map(|k| (k.to_string(), Value::Null)));
}
tree.insert("svg".into(), svg);
}
trees
}
/// The format rows (`formats`) and the per-format `settings` of Export for Screens params `p`, at
/// `within` (`settings.` in `document.exportPresets.save`). A row, and the settings of a format,
/// take [`screen_option_keys`]; `settings` takes the formats Export for Screens writes.
fn check_screen_rows(id: &str, within: &str, p: &serde_json::Map<String, Value>) -> Result<()> {
let keys = screen_option_keys();
let trees = &screen_option_trees();
if let Some(Value::Array(rows)) = p.get("formats") {
for (i, row) in rows.iter().enumerate() {
if let Value::Object(row) = row {
check_option_keys(id, &format!("{within}formats[{i}]."), row, &keys, NO_FORMAT, trees)?;
}
}
}
if let Some(Value::Object(settings)) = p.get("settings") {
let formats = screen_formats();
for (format, options) in settings {
if !formats.contains(&format.as_str()) {
let why = match nearest(format, &formats) {
Some(near) => format!("did you mean `{within}settings.{near}`?"),
None => format!("`{within}settings` takes: {}", formats.join(", ")),
};
return Err(bad(id, format!("unknown parameter `{within}settings.{format}`; {why}")));
}
if let Value::Object(options) = options {
check_option_keys(id, &format!("{within}settings.{format}."), options, &keys, NO_FORMAT, trees)?;
}
}
}
Ok(())
}
/// The options of an export or save command `id`. `document.export`, `document.exportSelection`
/// and `document.serialize` take a key that some format reads (one object can carry several
/// formats' options; `document.formats` lists them). `document.exportPdf`, `document.pdfSettings`
/// and `pdf.preset.save` take a key that PDF reads, `document.exportEps` one that EPS reads and
/// `document.exportDxf` one that DXF reads. `document.save`, `file.saveAs`, `file.saveCopy` and
/// `file.saveAsTemplate` take, beside the path and in `options`, a key that some save format
/// reads. `document.exportForScreens` takes the keys it reads, and checks its format rows and
/// settings ([`check_screen_rows`]), as `document.exportPresets.save` does in its `settings` and
/// `assets.export` and `assets.settings.set` do in theirs.
/// Where a command takes `marks`, `bleed`, `flattener`, `svg` or another object option of PDF,
/// EPS or SVG, only the object's own keys pass ([`option_trees`]). Saved presets are not checked:
/// one with a key that is no longer read keeps loading.
fn check_export_options(id: &str, params: &Value) -> Result<()> {
use super::fileio::{FORMATS, dxf, eps, pdf};
use super::fileio::{FORMATS, SAVE_FORMATS, SCREEN_PARAM_KEYS, TEMPLATE_FORMATS, dxf, eps, pdf};
let Value::Object(given) = params else { return Ok(()) };
match (id, given.get("settings")) {
("document.exportForScreens", _) => {
if let Some(key) = given.keys().find(|k| !SCREEN_PARAM_KEYS.contains(&k.as_str())) {
return Err(unknown(id, key, &SCREEN_PARAM_KEYS));
}
return check_screen_rows(id, "", given);
}
("document.exportPresets.save", Some(Value::Object(settings))) => {
let why = format!("`settings` takes: {}", SCREEN_PARAM_KEYS.join(", "));
check_option_keys(id, "settings.", settings, &SCREEN_PARAM_KEYS, &why, &Value::Null)?;
return check_screen_rows(id, "settings.", settings);
}
// Export Assets exports with, and stores, Export for Screens' format rows and settings.
("assets.export" | "assets.settings.set", _) => return check_screen_rows(id, "", given),
_ => {}
}
let mut keys: Vec<&str> = vec!["path", "format", "acknowledgeLoss"];
// A save's `options` and the keys they take.
let mut inner: Option<(&serde_json::Map<String, Value>, Vec<&str>)> = None;
let why_not = match id {
"document.save" | "file.saveAs" | "file.saveCopy" | "file.saveAsTemplate" => {
let saved = FORMATS.iter().filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id));
let options: Vec<&str> = saved.flat_map(|f| f.options).map(|o| o.name).collect();
if let Some(Value::Object(o)) = given.get("options") {
inner = Some((o, options.clone()));
}
keys.extend(["options", "modified"]);
keys.extend(options);
NO_SAVE_FORMAT
}
"document.export" | "document.exportSelection" | "document.serialize" => {
// `richBlack` is Appearance of Black's setting for a raster export, and `params` an
// exporter plug-in's own.
keys.extend(["includeArtboardFill", "selectedOnly", "richBlack", "params"]);
keys.extend(FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).map(|o| o.name));
"no format takes it"
NO_FORMAT
}
"document.exportPdf" | "document.pdfSettings" | "pdf.preset.save" => {
keys.push("includeArtboardFill");
@@ -559,38 +717,24 @@ fn check_export_options(id: &str, params: &Value) -> Result<()> {
"pdf.preset.save" => keys.extend(["name", "newName", "description"]),
_ => {}
}
"PDF doesn't take it"
"PDF doesn't take it (see document.formats)"
}
"document.exportEps" => {
keys.push("includeArtboardFill");
keys.extend(eps::OPTIONS.iter().map(|o| o.name));
"EPS doesn't take it"
"EPS doesn't take it (see document.formats)"
}
"document.exportDxf" => {
keys.extend(dxf::OPTIONS.iter().map(|o| o.name));
"DXF doesn't take it"
"DXF doesn't take it (see document.formats)"
}
_ => return Ok(()),
};
let mut trees = pdf_option_tree();
if let Some(tree) = trees.as_object_mut() {
tree.insert("flattener".into(), serde_json::to_value(super::flatten::FlattenOptions::default()).unwrap_or_default());
let trees = option_trees();
if let Some((o, options)) = inner {
check_option_keys(id, "options.", o, &options, NO_SAVE_FORMAT, &trees)?;
}
for (key, value) in given {
if !keys.contains(&key.as_str()) {
return Err(bad(
id,
match nearest(key, &keys) {
Some(near) => format!("unknown parameter `{key}`; did you mean `{near}`?"),
None => format!("unknown parameter `{key}`; {why_not} (see document.formats)"),
},
));
}
if let (Some(Value::Object(schema)), Value::Object(value)) = (trees.get(key), value) {
check_option_tree(id, &format!("{key}."), value, schema)?;
}
}
Ok(())
check_option_keys(id, "", given, &keys, why_not, &trees)
}
/// [`check_keys`] for an engine command, the steps of a `command.batch` or an `action.batch` included,
+202
View File
@@ -177,6 +177,208 @@ fn export_options_pass_every_listed_key() {
}
}
const SAVES: [&str; 4] = ["document.save", "file.saveAs", "file.saveCopy", "file.saveAsTemplate"];
/// A save refuses a key no save format reads, beside the path, in `options` and in the PDF and
/// SVG objects of either, under each save command.
#[test]
fn save_options_refuse_a_key_no_save_format_reads() {
let why = "no save format takes it (see file.formatOptions)";
for id in SAVES {
assert_eq!(refusal(id, json!({"path": "a.pdf", "noSuchOption": true})), format!("unknown parameter `noSuchOption`; {why}"));
assert_eq!(
refusal(id, json!({"path": "a.pdf", "options": {"noSuchOption": true}})),
format!("unknown parameter `options.noSuchOption`; {why}")
);
assert_eq!(refusal(id, json!({"compres": true})), "unknown parameter `compres`; did you mean `compress`?");
assert_eq!(refusal(id, json!({"options": {"compres": true}})), "unknown parameter `options.compres`; did you mean `options.compress`?");
// An export's own key is not a save's.
assert_eq!(refusal(id, json!({"path": "a.png", "quality": 80})), format!("unknown parameter `quality`; {why}"));
for (given, message) in [
(json!({"path": "a.pdf", "marks": {"useDocumentBleed": true}}), "unknown parameter `marks.useDocumentBleed`; `marks` takes:"),
(json!({"path": "a.pdf", "options": {"marks": {"x": 1}}}), "unknown parameter `options.marks.x`; `options.marks` takes:"),
(json!({"path": "a.ai", "bleed": {"useDocumen": true}}), "unknown parameter `bleed.useDocumen`; did you mean `bleed.useDocument`?"),
// The `svg` object is checked when the save writes no SVG too.
(json!({"path": "a.pdf", "svg": {"noSuchOption": true}}), "unknown parameter `svg.noSuchOption`; `svg` takes:"),
(
json!({"path": "a.pdf", "options": {"svg": {"minfy": true}}}),
"unknown parameter `options.svg.minfy`; did you mean `options.svg.minify`?",
),
] {
let got = refusal(id, given.clone());
assert!(got.starts_with(message), "{id} {given}: {got}");
}
// A key that another format a save writes reads still passes: the save leaves it out.
for given in [
json!({"path": "a.svg", "compress": true}),
json!({"path": "a.pdf", "options": {"preview": true, "styling": "style"}}),
json!({"path": "a.vectorcraft", "standard": "pdfX4", "marks": {"trim": true}}),
json!({"path": "a.ai", "acknowledgeLoss": true, "modified": null, "svg": {"minify": true}}),
json!({"path": "a.svg", "svg": {"styling": "css", "artboard": 0, "useArtboards": true}}),
] {
check_params(id, &given).unwrap_or_else(|e| panic!("{id} {given}: {e}"));
}
}
}
/// A save passes each option of each format it writes, beside the path and in `options`, and
/// each key its description names.
#[test]
fn save_options_pass_every_listed_key() {
use crate::cmd::fileio::{FORMATS, SAVE_FORMATS, TEMPLATE_FORMATS};
for f in FORMATS.iter().filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id)) {
for o in f.options {
for params in [json!({"format": f.id, o.name: null}), json!({"format": f.id, "options": {o.name: null}})] {
for id in SAVES {
check_params(id, &params).unwrap_or_else(|e| panic!("{id} {params}: {e}"));
}
}
}
}
for id in SAVES {
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
}
}
/// Export for Screens refuses a key it doesn't take, and in its rows and settings a key no format
/// reads, by the row's path; `document.exportPresets.save` checks the rows of its settings.
#[test]
fn export_for_screens_refuses_a_key_no_format_reads() {
let id = "document.exportForScreens";
assert!(
refusal(id, json!({"folder": "out", "noSuchOption": true})).starts_with("unknown parameter `noSuchOption`; the command takes: artboard, ")
);
assert_eq!(refusal(id, json!({"subfolder": true})), "unknown parameter `subfolder`; did you mean `subfolders`?");
let no_format = "no format takes it (see document.formats)";
for (given, message) in [
(json!({"formats": [{"format": "png", "noSuchOption": 1}]}), format!("unknown parameter `formats[0].noSuchOption`; {no_format}")),
(
json!({"formats": [{"format": "png"}, {"format": "pdf", "marks": {"useDocumentBleed": true}}]}),
"unknown parameter `formats[1].marks.useDocumentBleed`; `formats[1].marks` takes:".into(),
),
(
json!({"formats": [{"format": "pdf", "security": {"openPasswrd": "x"}}]}),
"unknown parameter `formats[0].security.openPasswrd`; did you mean `formats[0].security.openPassword`?".into(),
),
(
json!({"formats": [{"format": "eps", "flattener": {"balanse": 50}}]}),
"unknown parameter `formats[0].flattener.balanse`; did you mean `formats[0].flattener.balance`?".into(),
),
(json!({"formats": [{"format": "jpg", "sufix": "@2x"}]}), "unknown parameter `formats[0].sufix`; did you mean `formats[0].suffix`?".into()),
(json!({"settings": {"pnj": {}}}), "unknown parameter `settings.pnj`; did you mean `settings.png`?".into()),
(json!({"settings": {"dxf": {}}}), "unknown parameter `settings.dxf`; `settings` takes: ".into()),
(
json!({"settings": {"png": {"bckground": "white"}}}),
"unknown parameter `settings.png.bckground`; did you mean `settings.png.background`?".into(),
),
(json!({"settings": {"pdf": {"bleed": {"top": 9, "inner": 1}}}}), "unknown parameter `settings.pdf.bleed.inner`;".into()),
] {
let got = refusal(id, given.clone());
assert!(got.starts_with(&message), "{given}: {got}");
}
let save = "document.exportPresets.save";
assert_eq!(
refusal(save, json!({"name": "a", "settings": {"formats": [{"format": "png", "noSuchOption": 1}]}})),
format!("unknown parameter `settings.formats[0].noSuchOption`; {no_format}")
);
assert!(
refusal(save, json!({"name": "a", "settings": {"settings": {"pdf": {"marks": {"x": 1}}}}}))
.starts_with("unknown parameter `settings.settings.pdf.marks.x`;")
);
assert_eq!(
refusal(save, json!({"name": "a", "settings": {"formtas": [{"format": "png"}]}})),
"unknown parameter `settings.formtas`; did you mean `settings.formats`?"
);
assert!(
refusal(save, json!({"name": "a", "settings": {"noSuchOption": true}}))
.starts_with("unknown parameter `settings.noSuchOption`; `settings` takes: artboard, ")
);
// Export Assets takes the same rows and settings, and stores them.
for asset in ["assets.export", "assets.settings.set"] {
assert_eq!(
refusal(asset, json!({"formats": [{"format": "png", "noSuchOption": 1}]})),
format!("unknown parameter `formats[0].noSuchOption`; {no_format}")
);
assert!(refusal(asset, json!({"settings": {"pdf": {"marks": {"x": 1}}}})).starts_with("unknown parameter `settings.pdf.marks.x`;"));
check_params(asset, &json!({"formats": [{"format": "jpg", "scale": "2x", "quality": 80}]})).unwrap();
}
// An exporter plug-in's `params` in a row (its format is a plug-in's: tests/plugin_export.rs).
check_params(id, &json!({"formats": [{"format": "png", "params": {}}]})).unwrap();
// Keys the preset leaves out are still Export for Screens params.
check_params(save, &json!({"name": "a", "settings": {"zip": true, "formats": [{"format": "jpg", "quality": 80}]}})).unwrap();
// The encoders read Include Artboard Fill and Appearance of Black from a row or a format's
// settings.
for given in [
json!({"formats": [{"format": "png", "includeArtboardFill": false, "richBlack": true}]}),
json!({"settings": {"png": {"includeArtboardFill": false, "richBlack": true}}}),
] {
check_params(id, &given).unwrap_or_else(|e| panic!("{given}: {e}"));
}
}
/// Export for Screens passes each key its description names, each option of a format in a row or
/// in settings, and the built-in presets; `settings` takes the formats a row can write.
#[test]
fn export_for_screens_passes_every_listed_key() {
use crate::cmd::fileio::{FORMATS, check_screen_settings, screen_export_builtin_presets};
let id = "document.exportForScreens";
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
assert!(keys.len() > 20, "{keys:?}");
// The keys the check takes are the ones the description names.
let (mut named, mut taken) = (keys.clone(), crate::cmd::fileio::SCREEN_PARAM_KEYS.to_vec());
named.sort_unstable();
taken.sort_unstable();
assert_eq!(named, taken);
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
check_params(id, &json!({"artboard": 0, "openLocation": true})).unwrap();
let s = Session::new();
for f in FORMATS.iter().filter(|f| f.write) {
for o in f.options {
let row = json!({"formats": [{"format": "png", "scale": 2, "suffix": "", "folder": "x", o.name: null}]});
check_params(id, &row).unwrap_or_else(|e| panic!("{row}: {e}"));
}
let written = check_screen_settings(&s, &json!({"formats": [{"format": f.id}]})).is_ok();
let settings = json!({"settings": {f.id: {}}});
assert_eq!(check_params(id, &settings).is_ok(), written, "{settings}");
}
for preset in screen_export_builtin_presets() {
let settings = serde_json::Value::Object(preset.settings);
check_params(id, &settings).unwrap_or_else(|e| panic!("{}: {e}", preset.name));
check_params("document.exportPresets.save", &json!({"name": "a", "settings": settings})).unwrap();
}
}
/// A saved row loses the keys Export for Screens refuses in it, inside its object options too, and
/// keeps the others.
#[test]
fn a_saved_row_drops_the_keys_no_format_reads() {
use crate::cmd::params::drop_unread_row_options;
let serde_json::Value::Object(mut row) = json!({
"format": "pdf", "scale": "1x", "noSuchOption": 1, "params": {"x": 1},
"marks": {"useDocumentBleed": true, "trim": true}, "preset": "High Quality Print",
"security": {"passwordRequired": true, "noSuchLock": 1}
}) else {
panic!("an object")
};
drop_unread_row_options(&mut row);
assert_eq!(
serde_json::Value::Object(row.clone()),
json!({
"format": "pdf", "scale": "1x", "params": {"x": 1}, "marks": {"trim": true}, "preset": "High Quality Print",
"security": {"passwordRequired": true}
})
);
check_params("document.exportForScreens", &json!({"formats": [row]})).unwrap();
// Remembered security whose passwords were left out, with one given again.
let security = json!({"passwordRequired": true, "openPassword": "x"});
for id in ["document.exportForScreens", "assets.export", "assets.settings.set"] {
check_params(id, &json!({"formats": [{"format": "pdf", "security": security}], "settings": {"pdf": {"security": security}}})).unwrap();
}
}
/// `stroke.set` names every key it reads (its own, `item`, and `ids`/`id`/`target`), so a
/// misnamed one is refused instead of answering `objects: 1` with nothing changed.
#[test]
+1 -1
View File
@@ -72,7 +72,7 @@ fn save_as_takes_on_path_format_and_options() {
let d = dir("saveas");
let mut s = session();
let pdf = path(&d, "doc.pdf");
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "bogus": 1}})).unwrap();
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "preview": true}})).unwrap();
let st = s.doc().unwrap();
assert_eq!((st.format, st.title()), ("pdf", "doc.pdf".to_string()));
assert_eq!(Value::Object(st.save_options.clone()), json!({"range": "1"}), "only the format's own options are kept");
+37
View File
@@ -3399,6 +3399,23 @@ fn the_shown_stories_fixture_opens_with_its_layers() {
assert!(texts.iter().all(|t| t.contains('\u{2028}') && t.contains('\n')), "{texts:?}");
}
/// The fuzzed editing data with its layer, pattern and global colour named in code page 949 opens
/// with those names, read in that encoding, from its layers.
#[test]
fn the_editing_fixture_reads_names_in_a_legacy_encoding() {
// 레이어 1, then 빨강.
let text = editing_text()
.replace("(One) Ln", r"(\267\271\300\314\276\356 1) Ln")
.replace("(Dots)", r"(\273\241\260\255)")
.replace("(Black global)", r"(\273\241\260\255 global)");
let bytes = editing_eps(&text);
let r = vectorcraft_eps::import_names(&bytes, true, true, vectorcraft_eps::NameEncoding::Korean).unwrap();
let names: Vec<_> = r.document.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
assert_eq!(names, ["레이어 1", "Two"], "{:?}", r.warnings);
assert!(r.document.pattern("빨강").is_some(), "{:?}", r.warnings);
assert!(r.document.swatch("빨강 global").is_some_and(|s| s.global), "{:?}", r.warnings);
}
proptest! {
#![proptest_config(config())]
@@ -3475,6 +3492,26 @@ proptest! {
survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
}
/// Names of any bytes (a layer's, a pattern's, a global colour's) read in any legacy name
/// encoding: each byte written as an octal escape, so every code page sees bytes it doesn't
/// define, cut sequences and UTF-8 alike.
#[test]
fn eps_editing_data_names_in_any_encoding_never_panic(
name in prop::collection::vec(any::<u8>(), 0..24),
encoding in prop::sample::select(vectorcraft_eps::NameEncoding::LABELS.to_vec()),
) {
let escaped: String = name.iter().map(|b| format!("\\{b:03o}")).collect();
let text = editing_text()
.replace("(One) Ln", &format!("({escaped}) Ln"))
.replace("(Dots)", &format!("({escaped})"))
.replace("(Black global)", &format!("({escaped} global)"));
let bytes = editing_eps(&text);
let encoding = vectorcraft_eps::NameEncoding::from_label(encoding).unwrap();
survive("EPS editing data names in a legacy encoding", || {
vectorcraft_eps::import_names(&bytes, true, false, encoding).ok().map(|r| r.document)
})?;
}
/// A `.ai` whose envelopes are damaged or hostile: any kind, warp style, options word,
/// Fidelity and grid size, corners out of range or repeated, the contents block cut short.
#[test]
+21
View File
@@ -125,6 +125,27 @@ fn an_export_step_or_preset_with_a_key_no_longer_read_still_runs() {
s.execute("document.export", &json!({"format": "pdf", "preset": "Old"})).unwrap();
}
/// So does a save, or an Export for Screens, with a key that is no longer read, and an Export for
/// Screens preset file that holds one in a row still loads and exports. `Session::execute`
/// refuses the same options.
#[test]
fn a_save_or_screens_step_with_a_key_no_longer_read_still_runs() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let save = json!({"format": "pdf", "noSuchOption": true, "options": {"retired": 1, "marks": {"useDocumentBleed": true}}});
assert!(check_params("file.saveCopy", &save).is_err());
assert!(s.execute("file.saveCopy", &save).is_err());
s.replay("file.saveCopy", &save).unwrap();
let screens = json!({"noSuchOption": true, "formats": [{"format": "png", "retired": 1}], "settings": {"pdf": {"marks": {"retired": true}}}});
assert!(check_params("document.exportForScreens", &screens).is_err());
assert!(s.execute("document.exportForScreens", &screens).is_err());
s.replay("document.exportForScreens", &screens).unwrap();
let file = json!({"format": "vcexportpresets", "presets": [{"name": "Old", "settings": {"formats": [{"format": "png", "noLongerRead": 1}]}}]});
s.execute("document.exportPresets.import", &json!({"data": file.to_string()})).unwrap();
check_params("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
s.execute("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
}
/// So do an effect's own parameters (#1592): a journal or a file with a key the effect doesn't
/// take, or a value of another type, replays and opens; the effect reads what it knows, clamped.
#[test]
+16
View File
@@ -157,6 +157,22 @@ fn the_exporter_is_given_its_parameters_and_the_file_it_writes() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Export for Screens writes an exporter's format from a row, with the row's `params`, and takes
/// settings for that format.
#[test]
fn export_for_screens_takes_an_exporter_s_format_and_params() {
let mut s = session();
let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#;
install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.screens", r#"["tss"]"#, params), Echo::Params));
let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "plugin.test.screens", "params": {"density": 7}}]})).unwrap();
let file = &r["files"][0];
assert!(file["name"].as_str().unwrap().ends_with(".tss"), "{r}");
let got: Value = serde_json::from_slice(&vectorcraft_format::base64_decode(file["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
assert_eq!(got["density"], json!(7.0));
let settings = json!({"formats": [{"format": "plugin.test.screens"}], "settings": {"plugin.test.screens": {"params": {"density": 2}}}});
s.execute("document.exportForScreens", &settings).unwrap();
}
/// Only the selected objects, with `selectedOnly`, as for the built-in formats.
#[test]
fn selected_only_hands_over_the_selection() {