From 56cbdbb09c8372d08bd33b978100d97b64304006 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 11:11:20 -0800 Subject: [PATCH] MCP: SVG output under read roots refuses to name a linked image file outside them With automation read roots in force, an SVG or SVGZ that would name a linked image's file outside them is refused before any file, folder or reply is written, with "a linked image file is outside the read roots", which names no file or folder. An names the file with images: link, and with embed when the document holds no pixels for the image. With preserveEditing, the native document the SVG carries names every linked image and placed file, the relative paths kept of them and each data set's image, so those are checked too, and a stored brush entry that does not read as a brush is refused because its links cannot be checked. - The check runs where the linked JavaScript check runs: document.serialize, document.export, document.exportSelection, document.exportForScreens and assets.export, every SVG and SVGZ save, file.exportAs and the SVG Options dialog's Show Code. It also covers clipboard.exportSvg, and a Copy that an automation request asks for puts no SVG on the system clipboard when it would name such a file. - Only the art each file writes is checked: the selected objects with selectedOnly and Export Selection, the art over each chosen artboard, hidden layers and objects only when hiddenLayers is on (a save keeps them by default), never template layers or the symbol or opacity-mask editing layer, and the symbols, patterns and brushes that art uses. An image colorized with a solid fill whose pixels are gray or missing names no file, so it is not checked outside Preserve Editing. - Embedded pixels, a file inside the roots, a relative path that leads inside them and a URL are written as before, and the person at the keyboard exports every link as stored. Docs: docs/mcp.md, docs/target-app-parity.md and the params text; de and nl rows for the new message, next to the JavaScript message's rows. --- crates/engine/src/cmd/clipboard.rs | 29 +- crates/engine/src/cmd/fileio.rs | 3 +- crates/engine/src/cmd/fileio/access.rs | 43 +++ crates/engine/src/cmd/fileio/commands.rs | 8 +- crates/engine/src/cmd/fileio/export.rs | 8 +- crates/engine/src/cmd/fileio/save.rs | 16 +- crates/engine/src/cmd/fileio/screens.rs | 7 + crates/engine/src/cmd/fileio/svg.rs | 14 + crates/engine/src/cmd/links.rs | 107 ++++++- crates/engine/src/tests/links.rs | 347 ++++++++++++++++++++++ crates/mcp/src/ui_catalog.rs | 2 +- crates/ui-egui/src/dialogs/svg_options.rs | 50 +++- crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/io/export.rs | 1 + crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/sysclip.rs | 5 +- crates/ui-egui/src/tests/automation.rs | 40 +++ docs/mcp.md | 15 + docs/target-app-parity.md | 1 + 20 files changed, 678 insertions(+), 22 deletions(-) diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index 446634d21c..4e086569ae 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -32,7 +32,7 @@ use crate::DocState; pub fn specs() -> Vec { let mut v = vec![ - cmd!(query "clipboard.exportSvg", "Clipboard as SVG", [], None, "{} → {svg} the copied objects as standalone SVG (null when the clipboard is empty)", always, export_svg), + cmd!(query "clipboard.exportSvg", "Clipboard as SVG", [], None, "{} → {svg} the copied objects as standalone SVG (null when the clipboard is empty). Under automation read roots it is refused while the SVG would name a linked image's file outside them (an image whose pixels the clipboard doesn't hold; a relative path is followed from the document the objects came from while it is open)", always, export_svg), cmd!( query "clipboard.importSvg", "Load SVG into Clipboard", @@ -435,14 +435,37 @@ impl Clipboard { } impl Session { - /// The internal clipboard as standalone SVG (`None` when it is empty). + /// The internal clipboard as standalone SVG (`None` when it is empty, and when the SVG would name + /// a linked image's file the caller asking now may not be shown: [`Self::clipboard_svg_refused`]). pub fn clipboard_svg(&self) -> Option { + if self.clipboard_svg_refused() { + return None; + } self.clipboard.svg() } + + /// Would the internal clipboard's SVG name a linked image's file outside the read roots in + /// force? It names the file of an image whose pixels the clipboard doesn't hold. A relative + /// path is followed from the document the objects came from while it is open; otherwise it + /// counts as outside. + pub fn clipboard_svg_refused(&self) -> bool { + self.check_clipboard_svg("clipboard.exportSvg").is_err() + } + + /// [`Self::clipboard_svg_refused`] as `cmd`'s error, which names no file or folder. + fn check_clipboard_svg(&self, cmd: &str) -> Result<()> { + if crate::file_access::current().is_none() || !self.clipboard.has_art() { + return Ok(()); + } + let from = self.docs.iter().find(|d| Some(d.uid) == self.clipboard.source_doc); + let base = from.map_or_else(super::links::LinkBase::default, super::links::LinkBase::of); + super::fileio::check_images_in(cmd, "svg", &self.clipboard.to_document(), base, &json!({})) + } } fn export_svg(s: &mut Session, _: &Value) -> Result { - Ok(json!({ "svg": s.clipboard_svg() })) + s.check_clipboard_svg("clipboard.exportSvg")?; + Ok(json!({ "svg": s.clipboard.svg() })) } fn import_svg(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 84fb121d0c..aa811b06b0 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -121,9 +121,10 @@ mod commands; mod export_names; mod formats; +pub(crate) use access::check_images_in; #[cfg(not(target_arch = "wasm32"))] pub(crate) use access::list_dir; -pub use access::{check_linked_scripts, write_file_with, write_save_file, write_save_file_with}; +pub use access::{check_linked_images, check_linked_scripts, write_file_with, write_save_file, write_save_file_with}; #[cfg(all(test, not(target_arch = "wasm32")))] mod tests_readonly; diff --git a/crates/engine/src/cmd/fileio/access.rs b/crates/engine/src/cmd/fileio/access.rs index 203a2262b0..cb5b4e85cc 100644 --- a/crates/engine/src/cmd/fileio/access.rs +++ b/crates/engine/src/cmd/fileio/access.rs @@ -139,6 +139,49 @@ pub fn check_linked_scripts(cmd: &str, format: &str, st: &crate::DocState) -> Re Ok(()) } +/// The error for an SVG that would name a linked image's file outside the read roots. +pub(crate) const IMAGE_OUTSIDE: &str = "a linked image file is outside the read roots"; + +/// `cmd` may write `d`, the document an output of `st` writes (all of it, or the objects it +/// exports), in format `format` (a format id) with options `p`: any format but SVG and SVGZ, and +/// those too unless read roots are in force and the file would name a linked image's file outside +/// them (a relative path taken from `st`'s folder; a URL is not checked). An `` names the +/// file with SVG Options › Images set to Link, and with Embed when the document holds no pixels for +/// the image, for the images of the art each file draws (with chosen artboards, the art over each +/// one; hidden layers and objects only with `hiddenLayers`, template layers and the opacity-mask +/// and symbol editing layers never) and of the symbols, patterns and brushes that art uses; with +/// Preserve Editing Capabilities, the native document the file carries names every linked file, the relative paths kept of them +/// and each data set's image. The error names no file or folder. +pub fn check_linked_images(cmd: &str, format: &str, st: &crate::DocState, d: &vectorcraft_doc::Document, p: &Value) -> Result<()> { + check_images_in(cmd, format, d, crate::cmd::links::LinkBase::of(st), p) +} + +/// [`check_linked_images`] for `d`, a document whose links are looked for from `base` (the +/// objects on the clipboard, looked for from the document they came from). +pub(crate) fn check_images_in(cmd: &str, format: &str, d: &vectorcraft_doc::Document, base: crate::cmd::links::LinkBase, p: &Value) -> Result<()> { + if !matches!(format, "svg" | "svgz") || crate::file_access::current().is_none() { + return Ok(()); + } + // Options that don't read as the encoder's choices count as Link, Preserve Editing and hidden + // layers kept: the encoder refuses them anyway. + let (link, preserve, hidden) = match super::svg::options_map(p) { + Ok(m) => { + let on = |k: &str| m.get(k).is_some_and(|v| v.as_bool() != Some(false)); + (m.get("images").is_some_and(|v| v.as_str() != Some("embed")), on("preserveEditing"), on("hiddenLayers")) + } + Err(_) => (true, true, true), + }; + // The opacity-mask and symbol editing layers are never written. + let d = &*d.without_edit_modes(); + let names = |d: &vectorcraft_doc::Document| crate::cmd::links::svg_names_hidden_image(d, base, link, hidden); + if preserve && crate::cmd::links::names_hidden_file(d, base) + || names(d) && super::svg::drawn(d, p).is_none_or(|files| files.iter().any(|f| names(f))) + { + return Err(bad(cmd, IMAGE_OUTSIDE)); + } + Ok(()) +} + /// `path` checked for writing against the automation roots in force. #[cfg(not(target_arch = "wasm32"))] pub(crate) fn may_write(path: &str) -> Result<()> { diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index ca74243699..b1c9cf5e2d 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -27,7 +27,7 @@ pub fn specs() -> Vec { "Serialize Document", [], None, - "{format?: vectorcraft (default)|template|svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd (ait, a .ait template, is written by file.saveAsTemplate), …the format's options (see document.formats; SVG ones also as svg: {…}), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own)} → {text, warnings} for svg (plus dataBase64, the file, when its encoding isn't UTF-8), else {dataBase64, warnings}; an SVG of several artboards also gives files: [{name, text}], linked images linked: [{name, dataBase64}]. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked)", + "{format?: vectorcraft (default)|template|svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd (ait, a .ait template, is written by file.saveAsTemplate), …the format's options (see document.formats; SVG ones also as svg: {…}), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own)} → {text, warnings} for svg (plus dataBase64, the file, when its encoding isn't UTF-8), else {dataBase64, warnings}; an SVG of several artboards also gives files: [{name, text}], linked images linked: [{name, dataBase64}]. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked), or while the file would name a linked image's file outside them (images: link, preserveEditing, or an image whose pixels the document doesn't hold; a URL is not checked)", has_doc, export::serialize ), @@ -36,7 +36,7 @@ pub fn specs() -> Vec { "Export Document", [], None, - "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin., which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own), includeArtboardFill?: true (raster, PDF, SVG and EPS draw each exported artboard's background color behind its art, a transparent background too; false leaves it out), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; pdf/ai: without editing data, with a warning, and ai refuses pdfCompatible: false; SVG has it as an SVG option), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent; #rrggbbaa and #rgba are refused here and in matte), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts, variableData} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), …hpgl writes a plot of the artboard as pen moves for plotters and cutters (each visible path once along its line in plotter units; useArtboards: true one file per chosen artboard, false the bounds of the visible art), hpglVersion?: hpgl (default: IN, SP, PU, PD) | hpgl2 (also NP and PC pen colors, each pen's paths as one encoded polyline PE), curveTolerance?: 0.1 (points, 0.01 to 10), pens?: [\"#rrggbb\"…] (pen 1 first, up to 255; each path takes the nearest; default a pen per color in the order the art first draws in it, numbered alike in every file), origin?: bottomLeft (default) | center, sortPaths?: false (each pen's next path the nearest), insideFirst?: false (a path after the pen's paths that lie inside it), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked). Never changes the document's path", + "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin., which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own), includeArtboardFill?: true (raster, PDF, SVG and EPS draw each exported artboard's background color behind its art, a transparent background too; false leaves it out), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; pdf/ai: without editing data, with a warning, and ai refuses pdfCompatible: false; SVG has it as an SVG option), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent; #rrggbbaa and #rgba are refused here and in matte), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts, variableData} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), …hpgl writes a plot of the artboard as pen moves for plotters and cutters (each visible path once along its line in plotter units; useArtboards: true one file per chosen artboard, false the bounds of the visible art), hpglVersion?: hpgl (default: IN, SP, PU, PD) | hpgl2 (also NP and PC pen colors, each pen's paths as one encoded polyline PE), curveTolerance?: 0.1 (points, 0.01 to 10), pens?: [\"#rrggbb\"…] (pen 1 first, up to 255; each path takes the nearest; default a pen per color in the order the art first draws in it, numbered alike in every file), origin?: bottomLeft (default) | center, sortPaths?: false (each pen's next path the nearest), insideFirst?: false (a path after the pen's paths that lie inside it), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked), or while the file would name a linked image's file outside them (images: link, preserveEditing, or an image whose pixels the document doesn't hold; a URL is not checked). Never changes the document's path", has_doc, export::export ), @@ -45,7 +45,7 @@ pub fn specs() -> Vec { "Export Selection…", ["File"], None, - "{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, as document.export refuses them. In the app, with no params or {}, the save panel picks the file and its format's options dialog follows (Save PDF, SVG Options, the raster options, EPS Options, DXF Options, Text Export Options or an exporter plug-in's parameters; other formats are written at once), whose OK writes the selection: the call returns null while the dialog is open, and OK returns what file.exportAs returns, without format or bounds (Text Export Options: {path})", + "{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, or while the file would name a linked image's file outside them (an image among the selected objects), as document.export refuses them. In the app, with no params or {}, the save panel picks the file and its format's options dialog follows (Save PDF, SVG Options, the raster options, EPS Options, DXF Options, Text Export Options or an exporter plug-in's parameters; other formats are written at once), whose OK writes the selection: the call returns null while the dialog is open, and OK returns what file.exportAs returns, without format or bounds (Text Export Options: {path})", has_selection, export::export_selection ), @@ -54,7 +54,7 @@ pub fn specs() -> Vec { "Export for Screens", ["File", "Export"], None, - "{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboard?: index | artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), includeArtboardFill?: true (Include Artboard Fill: each artboard's background color behind its art; false leaves it out), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with for the document, for the artboard, asset, layer or object, from 1, #### for the counter zero-padded to the run of #, , , (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, ,