From 552205941717d295f529fcf8fd10b7af1d933d47 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 16:51:46 -0800 Subject: [PATCH 1/2] Save: the save commands and Export for Screens refuse an unknown option key document.save, file.saveAs, file.saveCopy and file.saveAsTemplate wrote the file when given a key that no save format reads, beside the path or in options, or an unknown key inside a PDF option object such as marks.useDocumentBleed. document.exportForScreens did the same for an unknown top-level key, a key in a format row and a key in the settings of a format. These commands now refuse such a key and name its path (options.noSuchOption, formats[1].marks.useDocumentBleed). The svg object is checked in every save, export and format row, also when the file written is not SVG. A save still leaves out a key that only another save format reads, and warns as before. A format row and a format's settings still take includeArtboardFill and richBlack, which the encoders read. document.exportPresets.save checks its settings, with the rows and settings in it, the same way. In the app, file.save and file.exportForScreens sent as commands are checked as document.save and document.exportForScreens, and app.save is checked before anything is written; MCP's app.save, file.save and file.exportForScreens are checked the same way. The SVG Options dialog no longer passes its internal fields on to the save. --- crates/engine/src/cmd/fileio.rs | 9 +- crates/engine/src/cmd/fileio/commands.rs | 4 +- crates/engine/src/cmd/fileio/encode.rs | 1 + crates/engine/src/cmd/fileio/save.rs | 2 +- crates/engine/src/cmd/fileio/screens.rs | 50 ++++++- crates/engine/src/cmd/params.rs | 165 +++++++++++++++++----- crates/engine/src/tests/params.rs | 156 ++++++++++++++++++++ crates/engine/src/tests/save.rs | 2 +- crates/engine/tests/it/param_keys.rs | 21 +++ crates/mcp/src/headless.rs | 8 +- crates/mcp/src/tests/exportas.rs | 32 +++++ crates/ui-egui/src/control.rs | 7 +- crates/ui-egui/src/dialogs/svg_options.rs | 3 +- crates/ui-egui/src/menus/run.rs | 6 + crates/ui-egui/src/tests/automation.rs | 34 +++++ docs/control-protocol.md | 2 +- docs/mcp.md | 31 +++- docs/target-app-parity.md | 1 + 18 files changed, 475 insertions(+), 59 deletions(-) diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 0c888d13c..b3dab1d12 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -83,16 +83,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 d698c3aad..7914fc846 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -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, ,