mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Merge main into fix/plugin-stale-grant
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,
|
||||
|
||||
@@ -177,6 +177,208 @@ fn export_options_pass_every_listed_key() {
|
||||
}
|
||||
}
|
||||
|
||||
const SAVES: [&str; 4] = ["document.save", "file.saveAs", "file.saveCopy", "file.saveAsTemplate"];
|
||||
|
||||
/// A save refuses a key no save format reads, beside the path, in `options` and in the PDF and
|
||||
/// SVG objects of either, under each save command.
|
||||
#[test]
|
||||
fn save_options_refuse_a_key_no_save_format_reads() {
|
||||
let why = "no save format takes it (see file.formatOptions)";
|
||||
for id in SAVES {
|
||||
assert_eq!(refusal(id, json!({"path": "a.pdf", "noSuchOption": true})), format!("unknown parameter `noSuchOption`; {why}"));
|
||||
assert_eq!(
|
||||
refusal(id, json!({"path": "a.pdf", "options": {"noSuchOption": true}})),
|
||||
format!("unknown parameter `options.noSuchOption`; {why}")
|
||||
);
|
||||
assert_eq!(refusal(id, json!({"compres": true})), "unknown parameter `compres`; did you mean `compress`?");
|
||||
assert_eq!(refusal(id, json!({"options": {"compres": true}})), "unknown parameter `options.compres`; did you mean `options.compress`?");
|
||||
// An export's own key is not a save's.
|
||||
assert_eq!(refusal(id, json!({"path": "a.png", "quality": 80})), format!("unknown parameter `quality`; {why}"));
|
||||
for (given, message) in [
|
||||
(json!({"path": "a.pdf", "marks": {"useDocumentBleed": true}}), "unknown parameter `marks.useDocumentBleed`; `marks` takes:"),
|
||||
(json!({"path": "a.pdf", "options": {"marks": {"x": 1}}}), "unknown parameter `options.marks.x`; `options.marks` takes:"),
|
||||
(json!({"path": "a.ai", "bleed": {"useDocumen": true}}), "unknown parameter `bleed.useDocumen`; did you mean `bleed.useDocument`?"),
|
||||
// The `svg` object is checked when the save writes no SVG too.
|
||||
(json!({"path": "a.pdf", "svg": {"noSuchOption": true}}), "unknown parameter `svg.noSuchOption`; `svg` takes:"),
|
||||
(
|
||||
json!({"path": "a.pdf", "options": {"svg": {"minfy": true}}}),
|
||||
"unknown parameter `options.svg.minfy`; did you mean `options.svg.minify`?",
|
||||
),
|
||||
] {
|
||||
let got = refusal(id, given.clone());
|
||||
assert!(got.starts_with(message), "{id} {given}: {got}");
|
||||
}
|
||||
// A key that another format a save writes reads still passes: the save leaves it out.
|
||||
for given in [
|
||||
json!({"path": "a.svg", "compress": true}),
|
||||
json!({"path": "a.pdf", "options": {"preview": true, "styling": "style"}}),
|
||||
json!({"path": "a.vectorcraft", "standard": "pdfX4", "marks": {"trim": true}}),
|
||||
json!({"path": "a.ai", "acknowledgeLoss": true, "modified": null, "svg": {"minify": true}}),
|
||||
json!({"path": "a.svg", "svg": {"styling": "css", "artboard": 0, "useArtboards": true}}),
|
||||
] {
|
||||
check_params(id, &given).unwrap_or_else(|e| panic!("{id} {given}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A save passes each option of each format it writes, beside the path and in `options`, and
|
||||
/// each key its description names.
|
||||
#[test]
|
||||
fn save_options_pass_every_listed_key() {
|
||||
use crate::cmd::fileio::{FORMATS, SAVE_FORMATS, TEMPLATE_FORMATS};
|
||||
for f in FORMATS.iter().filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id)) {
|
||||
for o in f.options {
|
||||
for params in [json!({"format": f.id, o.name: null}), json!({"format": f.id, "options": {o.name: null}})] {
|
||||
for id in SAVES {
|
||||
check_params(id, ¶ms).unwrap_or_else(|e| panic!("{id} {params}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in SAVES {
|
||||
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
|
||||
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
|
||||
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export for Screens refuses a key it doesn't take, and in its rows and settings a key no format
|
||||
/// reads, by the row's path; `document.exportPresets.save` checks the rows of its settings.
|
||||
#[test]
|
||||
fn export_for_screens_refuses_a_key_no_format_reads() {
|
||||
let id = "document.exportForScreens";
|
||||
assert!(
|
||||
refusal(id, json!({"folder": "out", "noSuchOption": true})).starts_with("unknown parameter `noSuchOption`; the command takes: artboard, ")
|
||||
);
|
||||
assert_eq!(refusal(id, json!({"subfolder": true})), "unknown parameter `subfolder`; did you mean `subfolders`?");
|
||||
let no_format = "no format takes it (see document.formats)";
|
||||
for (given, message) in [
|
||||
(json!({"formats": [{"format": "png", "noSuchOption": 1}]}), format!("unknown parameter `formats[0].noSuchOption`; {no_format}")),
|
||||
(
|
||||
json!({"formats": [{"format": "png"}, {"format": "pdf", "marks": {"useDocumentBleed": true}}]}),
|
||||
"unknown parameter `formats[1].marks.useDocumentBleed`; `formats[1].marks` takes:".into(),
|
||||
),
|
||||
(
|
||||
json!({"formats": [{"format": "pdf", "security": {"openPasswrd": "x"}}]}),
|
||||
"unknown parameter `formats[0].security.openPasswrd`; did you mean `formats[0].security.openPassword`?".into(),
|
||||
),
|
||||
(
|
||||
json!({"formats": [{"format": "eps", "flattener": {"balanse": 50}}]}),
|
||||
"unknown parameter `formats[0].flattener.balanse`; did you mean `formats[0].flattener.balance`?".into(),
|
||||
),
|
||||
(json!({"formats": [{"format": "jpg", "sufix": "@2x"}]}), "unknown parameter `formats[0].sufix`; did you mean `formats[0].suffix`?".into()),
|
||||
(json!({"settings": {"pnj": {}}}), "unknown parameter `settings.pnj`; did you mean `settings.png`?".into()),
|
||||
(json!({"settings": {"dxf": {}}}), "unknown parameter `settings.dxf`; `settings` takes: ".into()),
|
||||
(
|
||||
json!({"settings": {"png": {"bckground": "white"}}}),
|
||||
"unknown parameter `settings.png.bckground`; did you mean `settings.png.background`?".into(),
|
||||
),
|
||||
(json!({"settings": {"pdf": {"bleed": {"top": 9, "inner": 1}}}}), "unknown parameter `settings.pdf.bleed.inner`;".into()),
|
||||
] {
|
||||
let got = refusal(id, given.clone());
|
||||
assert!(got.starts_with(&message), "{given}: {got}");
|
||||
}
|
||||
let save = "document.exportPresets.save";
|
||||
assert_eq!(
|
||||
refusal(save, json!({"name": "a", "settings": {"formats": [{"format": "png", "noSuchOption": 1}]}})),
|
||||
format!("unknown parameter `settings.formats[0].noSuchOption`; {no_format}")
|
||||
);
|
||||
assert!(
|
||||
refusal(save, json!({"name": "a", "settings": {"settings": {"pdf": {"marks": {"x": 1}}}}}))
|
||||
.starts_with("unknown parameter `settings.settings.pdf.marks.x`;")
|
||||
);
|
||||
assert_eq!(
|
||||
refusal(save, json!({"name": "a", "settings": {"formtas": [{"format": "png"}]}})),
|
||||
"unknown parameter `settings.formtas`; did you mean `settings.formats`?"
|
||||
);
|
||||
assert!(
|
||||
refusal(save, json!({"name": "a", "settings": {"noSuchOption": true}}))
|
||||
.starts_with("unknown parameter `settings.noSuchOption`; `settings` takes: artboard, ")
|
||||
);
|
||||
// Export Assets takes the same rows and settings, and stores them.
|
||||
for asset in ["assets.export", "assets.settings.set"] {
|
||||
assert_eq!(
|
||||
refusal(asset, json!({"formats": [{"format": "png", "noSuchOption": 1}]})),
|
||||
format!("unknown parameter `formats[0].noSuchOption`; {no_format}")
|
||||
);
|
||||
assert!(refusal(asset, json!({"settings": {"pdf": {"marks": {"x": 1}}}})).starts_with("unknown parameter `settings.pdf.marks.x`;"));
|
||||
check_params(asset, &json!({"formats": [{"format": "jpg", "scale": "2x", "quality": 80}]})).unwrap();
|
||||
}
|
||||
// An exporter plug-in's `params` in a row (its format is a plug-in's: tests/plugin_export.rs).
|
||||
check_params(id, &json!({"formats": [{"format": "png", "params": {}}]})).unwrap();
|
||||
// Keys the preset leaves out are still Export for Screens params.
|
||||
check_params(save, &json!({"name": "a", "settings": {"zip": true, "formats": [{"format": "jpg", "quality": 80}]}})).unwrap();
|
||||
// The encoders read Include Artboard Fill and Appearance of Black from a row or a format's
|
||||
// settings.
|
||||
for given in [
|
||||
json!({"formats": [{"format": "png", "includeArtboardFill": false, "richBlack": true}]}),
|
||||
json!({"settings": {"png": {"includeArtboardFill": false, "richBlack": true}}}),
|
||||
] {
|
||||
check_params(id, &given).unwrap_or_else(|e| panic!("{given}: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Export for Screens passes each key its description names, each option of a format in a row or
|
||||
/// in settings, and the built-in presets; `settings` takes the formats a row can write.
|
||||
#[test]
|
||||
fn export_for_screens_passes_every_listed_key() {
|
||||
use crate::cmd::fileio::{FORMATS, check_screen_settings, screen_export_builtin_presets};
|
||||
let id = "document.exportForScreens";
|
||||
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
|
||||
assert!(keys.len() > 20, "{keys:?}");
|
||||
// The keys the check takes are the ones the description names.
|
||||
let (mut named, mut taken) = (keys.clone(), crate::cmd::fileio::SCREEN_PARAM_KEYS.to_vec());
|
||||
named.sort_unstable();
|
||||
taken.sort_unstable();
|
||||
assert_eq!(named, taken);
|
||||
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
|
||||
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
|
||||
check_params(id, &json!({"artboard": 0, "openLocation": true})).unwrap();
|
||||
let s = Session::new();
|
||||
for f in FORMATS.iter().filter(|f| f.write) {
|
||||
for o in f.options {
|
||||
let row = json!({"formats": [{"format": "png", "scale": 2, "suffix": "", "folder": "x", o.name: null}]});
|
||||
check_params(id, &row).unwrap_or_else(|e| panic!("{row}: {e}"));
|
||||
}
|
||||
let written = check_screen_settings(&s, &json!({"formats": [{"format": f.id}]})).is_ok();
|
||||
let settings = json!({"settings": {f.id: {}}});
|
||||
assert_eq!(check_params(id, &settings).is_ok(), written, "{settings}");
|
||||
}
|
||||
for preset in screen_export_builtin_presets() {
|
||||
let settings = serde_json::Value::Object(preset.settings);
|
||||
check_params(id, &settings).unwrap_or_else(|e| panic!("{}: {e}", preset.name));
|
||||
check_params("document.exportPresets.save", &json!({"name": "a", "settings": settings})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved row loses the keys Export for Screens refuses in it, inside its object options too, and
|
||||
/// keeps the others.
|
||||
#[test]
|
||||
fn a_saved_row_drops_the_keys_no_format_reads() {
|
||||
use crate::cmd::params::drop_unread_row_options;
|
||||
let serde_json::Value::Object(mut row) = json!({
|
||||
"format": "pdf", "scale": "1x", "noSuchOption": 1, "params": {"x": 1},
|
||||
"marks": {"useDocumentBleed": true, "trim": true}, "preset": "High Quality Print",
|
||||
"security": {"passwordRequired": true, "noSuchLock": 1}
|
||||
}) else {
|
||||
panic!("an object")
|
||||
};
|
||||
drop_unread_row_options(&mut row);
|
||||
assert_eq!(
|
||||
serde_json::Value::Object(row.clone()),
|
||||
json!({
|
||||
"format": "pdf", "scale": "1x", "params": {"x": 1}, "marks": {"trim": true}, "preset": "High Quality Print",
|
||||
"security": {"passwordRequired": true}
|
||||
})
|
||||
);
|
||||
check_params("document.exportForScreens", &json!({"formats": [row]})).unwrap();
|
||||
// Remembered security whose passwords were left out, with one given again.
|
||||
let security = json!({"passwordRequired": true, "openPassword": "x"});
|
||||
for id in ["document.exportForScreens", "assets.export", "assets.settings.set"] {
|
||||
check_params(id, &json!({"formats": [{"format": "pdf", "security": security}], "settings": {"pdf": {"security": security}}})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// `stroke.set` names every key it reads (its own, `item`, and `ids`/`id`/`target`), so a
|
||||
/// misnamed one is refused instead of answering `objects: 1` with nothing changed.
|
||||
#[test]
|
||||
|
||||
@@ -72,7 +72,7 @@ fn save_as_takes_on_path_format_and_options() {
|
||||
let d = dir("saveas");
|
||||
let mut s = session();
|
||||
let pdf = path(&d, "doc.pdf");
|
||||
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "bogus": 1}})).unwrap();
|
||||
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "preview": true}})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((st.format, st.title()), ("pdf", "doc.pdf".to_string()));
|
||||
assert_eq!(Value::Object(st.save_options.clone()), json!({"range": "1"}), "only the format's own options are kept");
|
||||
|
||||
@@ -3399,6 +3399,23 @@ fn the_shown_stories_fixture_opens_with_its_layers() {
|
||||
assert!(texts.iter().all(|t| t.contains('\u{2028}') && t.contains('\n')), "{texts:?}");
|
||||
}
|
||||
|
||||
/// The fuzzed editing data with its layer, pattern and global colour named in code page 949 opens
|
||||
/// with those names, read in that encoding, from its layers.
|
||||
#[test]
|
||||
fn the_editing_fixture_reads_names_in_a_legacy_encoding() {
|
||||
// 레이어 1, then 빨강.
|
||||
let text = editing_text()
|
||||
.replace("(One) Ln", r"(\267\271\300\314\276\356 1) Ln")
|
||||
.replace("(Dots)", r"(\273\241\260\255)")
|
||||
.replace("(Black global)", r"(\273\241\260\255 global)");
|
||||
let bytes = editing_eps(&text);
|
||||
let r = vectorcraft_eps::import_names(&bytes, true, true, vectorcraft_eps::NameEncoding::Korean).unwrap();
|
||||
let names: Vec<_> = r.document.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
|
||||
assert_eq!(names, ["레이어 1", "Two"], "{:?}", r.warnings);
|
||||
assert!(r.document.pattern("빨강").is_some(), "{:?}", r.warnings);
|
||||
assert!(r.document.swatch("빨강 global").is_some_and(|s| s.global), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
@@ -3475,6 +3492,26 @@ proptest! {
|
||||
survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// Names of any bytes (a layer's, a pattern's, a global colour's) read in any legacy name
|
||||
/// encoding: each byte written as an octal escape, so every code page sees bytes it doesn't
|
||||
/// define, cut sequences and UTF-8 alike.
|
||||
#[test]
|
||||
fn eps_editing_data_names_in_any_encoding_never_panic(
|
||||
name in prop::collection::vec(any::<u8>(), 0..24),
|
||||
encoding in prop::sample::select(vectorcraft_eps::NameEncoding::LABELS.to_vec()),
|
||||
) {
|
||||
let escaped: String = name.iter().map(|b| format!("\\{b:03o}")).collect();
|
||||
let text = editing_text()
|
||||
.replace("(One) Ln", &format!("({escaped}) Ln"))
|
||||
.replace("(Dots)", &format!("({escaped})"))
|
||||
.replace("(Black global)", &format!("({escaped} global)"));
|
||||
let bytes = editing_eps(&text);
|
||||
let encoding = vectorcraft_eps::NameEncoding::from_label(encoding).unwrap();
|
||||
survive("EPS editing data names in a legacy encoding", || {
|
||||
vectorcraft_eps::import_names(&bytes, true, false, encoding).ok().map(|r| r.document)
|
||||
})?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose envelopes are damaged or hostile: any kind, warp style, options word,
|
||||
/// Fidelity and grid size, corners out of range or repeated, the contents block cut short.
|
||||
#[test]
|
||||
|
||||
@@ -125,6 +125,27 @@ fn an_export_step_or_preset_with_a_key_no_longer_read_still_runs() {
|
||||
s.execute("document.export", &json!({"format": "pdf", "preset": "Old"})).unwrap();
|
||||
}
|
||||
|
||||
/// So does a save, or an Export for Screens, with a key that is no longer read, and an Export for
|
||||
/// Screens preset file that holds one in a row still loads and exports. `Session::execute`
|
||||
/// refuses the same options.
|
||||
#[test]
|
||||
fn a_save_or_screens_step_with_a_key_no_longer_read_still_runs() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let save = json!({"format": "pdf", "noSuchOption": true, "options": {"retired": 1, "marks": {"useDocumentBleed": true}}});
|
||||
assert!(check_params("file.saveCopy", &save).is_err());
|
||||
assert!(s.execute("file.saveCopy", &save).is_err());
|
||||
s.replay("file.saveCopy", &save).unwrap();
|
||||
let screens = json!({"noSuchOption": true, "formats": [{"format": "png", "retired": 1}], "settings": {"pdf": {"marks": {"retired": true}}}});
|
||||
assert!(check_params("document.exportForScreens", &screens).is_err());
|
||||
assert!(s.execute("document.exportForScreens", &screens).is_err());
|
||||
s.replay("document.exportForScreens", &screens).unwrap();
|
||||
let file = json!({"format": "vcexportpresets", "presets": [{"name": "Old", "settings": {"formats": [{"format": "png", "noLongerRead": 1}]}}]});
|
||||
s.execute("document.exportPresets.import", &json!({"data": file.to_string()})).unwrap();
|
||||
check_params("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
|
||||
s.execute("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
|
||||
}
|
||||
|
||||
/// So do an effect's own parameters (#1592): a journal or a file with a key the effect doesn't
|
||||
/// take, or a value of another type, replays and opens; the effect reads what it knows, clamped.
|
||||
#[test]
|
||||
|
||||
@@ -157,6 +157,22 @@ fn the_exporter_is_given_its_parameters_and_the_file_it_writes() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Export for Screens writes an exporter's format from a row, with the row's `params`, and takes
|
||||
/// settings for that format.
|
||||
#[test]
|
||||
fn export_for_screens_takes_an_exporter_s_format_and_params() {
|
||||
let mut s = session();
|
||||
let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#;
|
||||
install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.screens", r#"["tss"]"#, params), Echo::Params));
|
||||
let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "plugin.test.screens", "params": {"density": 7}}]})).unwrap();
|
||||
let file = &r["files"][0];
|
||||
assert!(file["name"].as_str().unwrap().ends_with(".tss"), "{r}");
|
||||
let got: Value = serde_json::from_slice(&vectorcraft_format::base64_decode(file["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
|
||||
assert_eq!(got["density"], json!(7.0));
|
||||
let settings = json!({"formats": [{"format": "plugin.test.screens"}], "settings": {"plugin.test.screens": {"params": {"density": 2}}}});
|
||||
s.execute("document.exportForScreens", &settings).unwrap();
|
||||
}
|
||||
|
||||
/// Only the selected objects, with `selectedOnly`, as for the built-in formats.
|
||||
#[test]
|
||||
fn selected_only_hands_over_the_selection() {
|
||||
|
||||
Reference in New Issue
Block a user