diff --git a/Cargo.lock b/Cargo.lock index e9d3ad332..798c159d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1059,7 +1059,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e#c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e" +source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d" dependencies = [ "chrono", "dunce", @@ -6511,6 +6511,7 @@ name = "vectorcraft-eps" version = "0.21.0" dependencies = [ "craft-pdf", + "encoding_rs", "fax", "flate2", "hayro-ccitt", diff --git a/Cargo.toml b/Cargo.toml index fd8527fd9..9de669345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ craft-window = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63 # The `.vectorcraft` envelope: its members, version, embedded images and the data after the JSON. craft-vectorcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] } # internal vectorcraft-geom = { path = "crates/geom" } vectorcraft-color = { path = "crates/color" } @@ -99,6 +99,9 @@ ruzstd = { version = "0.9.1", default-features = false, features = ["std"] } hayro-ccitt = "0.3" # Substring search for the EPS reader's PostScript `search`. memchr = "2" +# The WHATWG Encoding Standard's legacy encodings: names in legacy .ai files read in the code page +# the user picks (Mac OS Roman, Korean, Japanese, Chinese). +encoding_rs = "0.8.42" jpeg-encoder = { version = "0.6", default-features = false, features = ["std"] } gif = { version = "0.14", default-features = false, features = ["std", "raii_no_panic"] } # CMYK image samples (vectorcraft-doc's cmyk module): the image crate's own TIFF and JPEG diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 0140d6e9c..7e66b0500 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -5,7 +5,7 @@ //! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... //! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] [--ppi 300] //! vectorcraft-cli commands -//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] +//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] //! vectorcraft-cli info FILE //! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5] //! vectorcraft-cli perf [--paths 50000] [--deep] @@ -107,7 +107,7 @@ USAGE: vectorcraft-cli commands Print the command catalogue as JSON. - vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] + vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] Open IN (any readable format) and export OUT in the format its extension picks (see Writable formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects resolution; other formats leave them out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard @@ -120,7 +120,10 @@ USAGE: IN left things out (hidden text, art or layers it could not read; the notes say which) unless --replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy even where that draws differently from its PDF part or page (which can be older), as long as - it can be read. A PDF OUT omits the embedded native document unless --preserve-editing is + it can be read. --legacy-name-encoding reads the names in a .ai's or an EPS's editing data + that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles, + swatches, brushes, symbols, variables and data sets, and data sets' text values) in + windows-1252 (the default), macintosh, windows-949, shift_jis, gbk or big5. A PDF OUT omits the embedded native document unless --preserve-editing is given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the attachment is written the result's warnings say so (it is left out, with its own warning, for --artboard or a --range without every artboard). @@ -321,8 +324,20 @@ fn commands() -> Result<(), String> { } fn convert(args: &[String]) -> Result<(), String> { - let valued = - ["--scale", "-s", "--ppi", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"]; + let valued = [ + "--scale", + "-s", + "--ppi", + "--artboard", + "-a", + "--range", + "-r", + "--decimals", + "--ai-version", + "--hidden-objects", + "--transparent-images", + "--legacy-name-encoding", + ]; if help_requested(args, &valued) { return help(); } @@ -331,6 +346,9 @@ fn convert(args: &[String]) -> Result<(), String> { (None::, None::, None::, false, false, false, false); let mut ppi = None::; let mut decimals = None::; + // The encoding of a legacy `.ai`'s names that aren't UTF-8, passed on as `document.open`'s + // `legacyNameEncoding`; the engine checks it. + let mut legacy_names = None::; // Legacy `.ai` options, passed on as the export's own (`aiVersion`, `hiddenObjects`, // `transparentImages`); the engine checks their values. let mut legacy: Vec<(&str, String)> = vec![]; @@ -353,6 +371,9 @@ fn convert(args: &[String]) -> Result<(), String> { "--replace-lossy" => replace_lossy = true, "--editing-data" => editing_data = true, "--preserve-editing" => preserve_editing = true, + "--legacy-name-encoding" => { + legacy_names = Some(it.next().cloned().ok_or("--legacy-name-encoding needs an encoding such as windows-949")?); + } "--ai-version" => legacy.push(("aiVersion", it.next().cloned().ok_or("--ai-version needs 7 or pdf")?)), "--hidden-objects" => legacy.push(("hiddenObjects", it.next().cloned().ok_or("--hidden-objects needs layer, omit or visible")?)), "--transparent-images" => legacy.push(("transparentImages", it.next().cloned().ok_or("--transparent-images needs flatten or omit")?)), @@ -365,8 +386,14 @@ fn convert(args: &[String]) -> Result<(), String> { return Err("a .ait template holds every artboard: --artboard and --range don't apply".into()); } let mut h = Headless::new(); - let opened = if editing_data { - let params = json!({"path": input, "editingData": "always"}); + let opened = if editing_data || legacy_names.is_some() { + let mut params = json!({"path": input}); + if editing_data { + params["editingData"] = json!("always"); + } + if let Some(e) = &legacy_names { + params["legacyNameEncoding"] = json!(e); + } h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))? } else { h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))? diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index f099b5f95..14bda9a00 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -128,11 +128,14 @@ fn live_inputs() -> u64 { } /// A live object evaluated by [`live_art`] on this thread: the object, its document's symbols (by -/// name and art) and patterns and the [`LiveInputs`] as they were, the art and its [`art_size`]. +/// name and art), patterns, swatches and color groups and the [`LiveInputs`] as they were, the art +/// and its [`art_size`]. struct EvaluatedLive { node: Node, symbols: Vec<(String, Arc)>, patterns: Vec, + swatches: crate::Shared>, + swatch_groups: crate::Shared>, inputs: u64, art: Rc>, size: usize, @@ -146,6 +149,8 @@ impl EvaluatedLive { && self.symbols.iter().zip(&doc.symbols).all(|((name, art), s)| *name == s.name && Arc::ptr_eq(art, &s.art)) && self.node == *n && self.patterns == doc.patterns + && self.swatches == doc.swatches + && self.swatch_groups == doc.swatch_groups } } @@ -184,8 +189,8 @@ thread_local! { /// The art live object `n` of `doc` draws ([`set_live_art`]); [`crate::live::expand_live`] while /// nothing is registered. The art is evaluated again when `n`, a symbol of `doc`, a pattern of -/// `doc` or the [`LiveInputs`] have changed since, and on every click while it or the document's -/// symbols or patterns hold a placed document ([`holds_placed`]). +/// `doc`, its swatches or color groups, or the [`LiveInputs`] have changed since, and on every +/// click while it or the document's symbols or patterns hold a placed document ([`holds_placed`]). fn live_art(doc: &Document, n: &Node) -> Rc> { let inputs = live_inputs(); let kept = EVALUATED.with_borrow_mut(|kept| { @@ -206,7 +211,16 @@ fn live_art(doc: &Document, n: &Node) -> Rc> { return art; } let symbols = doc.symbols.iter().map(|s| (s.name.clone(), s.art.clone())).collect(); - let e = EvaluatedLive { node: n.clone(), symbols, patterns: doc.patterns.clone(), inputs, art: art.clone(), size }; + let e = EvaluatedLive { + node: n.clone(), + symbols, + patterns: doc.patterns.clone(), + swatches: doc.swatches.clone(), + swatch_groups: doc.swatch_groups.clone(), + inputs, + art: art.clone(), + size, + }; EVALUATED.with_borrow_mut(|kept| { kept.insert(0, e); kept.truncate(LIVE_KEPT); diff --git a/crates/doc/src/hit/tests.rs b/crates/doc/src/hit/tests.rs index d6f4cafe7..cb5b19d1a 100644 --- a/crates/doc/src/hit/tests.rs +++ b/crates/doc/src/hit/tests.rs @@ -605,11 +605,41 @@ fn evaluated_art_is_not_kept_while_a_placed_document_may_show_in_it() { fn a_kept_evaluation_is_stale_after_the_live_inputs_change() { let d = Document::new(100.0, 100.0); let n = Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()); - let e = EvaluatedLive { node: n.clone(), symbols: vec![], patterns: vec![], inputs: 7, art: std::rc::Rc::new(vec![]), size: 0 }; + let e = kept_evaluation(&d, &n); assert!(e.is_of(&d, &n, 7)); assert!(!e.is_of(&d, &n, 8)); } +/// An evaluation of `n` in `d`, with inputs 7, holding no symbols, patterns or art. +fn kept_evaluation(d: &Document, n: &Node) -> EvaluatedLive { + EvaluatedLive { + node: n.clone(), + symbols: vec![], + patterns: vec![], + swatches: d.swatches.clone(), + swatch_groups: d.swatch_groups.clone(), + inputs: 7, + art: std::rc::Rc::new(vec![]), + size: 0, + } +} + +/// A kept evaluation is stale once a swatch changes, in a color group too: a painted live effect's +/// pieces name swatches. +#[test] +fn a_kept_evaluation_is_stale_after_a_swatch_changes() { + let mut d = Document::new(100.0, 100.0); + let n = Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()); + let e = kept_evaluation(&d, &n); + assert!(e.is_of(&d, &n, 7)); + d.swatches.first_mut().unwrap().name = "Renamed".into(); + assert!(!e.is_of(&d, &n, 7)); + let mut d = Document::new(100.0, 100.0); + let e = kept_evaluation(&d, &n); + d.swatch_groups.push(vectorcraft_color::SwatchGroup { name: "Added".into(), swatches: vec![] }); + assert!(!e.is_of(&d, &n, 7)); +} + /// A rectangle (40, 40)–(140, 140) clipping a filled 200 × 200 square drawn over its edges, in a /// clip group (or, `layer`, as the first layer's clipping mask): the group (or the layer), the /// clipping path and the art. diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index d11df03be..b913720f1 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -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. diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index 52478ac13..ce7a82d2b 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -9,7 +9,7 @@ pub fn specs() -> Vec { "Open Document", [], None, - "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)", + "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), legacyNameEncoding?: windows-1252 (default) | macintosh | windows-949 (also euc-kr) | shift_jis | gbk | big5 (the code page names in the editing data of a .ai or an EPS are read in when they aren't UTF-8: layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets, and data sets' text values; warnings list the first three such names as read, and count the rest), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)", always, load::open ), @@ -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}), 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, ,