mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
Merge main into fix/plugin-grant-app-approval
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user