M4.44: PDF presets: built-ins in code, saved presets, Edit > PDF Presets

Built-in presets are generated in code (VectorCraft Default, High
Quality Print, Press Quality, Smallest File Size and PDF/X-1a, X-3,
X-4, the latter listed as not supported yet) and read-only. Saved
presets live with the preferences (Prefs.pdf_presets) and work as
`preset` wherever PDF options are taken: document.exportPdf,
pdfSettings, export/serialize as PDF and .ai saves.

pdf.preset.list / save / delete / export / import manage them
(.vcpdfpresets files; opening one imports it). The Save PDF dialog's
Save Preset… saves its settings under a name; Edit > PDF Presets…
(ui.pdfPresetsDialog) lists the presets with each one's description and
differences from the default, with New…, Edit… (the Save PDF dialog as
a preset editor, ui.pdfPresetDialog), Delete, Import… and Export….
document.pdfSettings now lists what differs from the chosen preset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 10:40:22 +07:00
co-authored by Claude Opus 5.5
parent 0ffaa427d1
commit 9aa29aa0fd
20 changed files with 1028 additions and 42 deletions
+2 -1
View File
@@ -306,12 +306,13 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
}
/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
/// libraries (which open in the library panel) and flattener presets (imported).
/// libraries (which open in the library panel), flattener presets and PDF presets (imported).
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
std::iter::once(("All readable files", OPEN_EXTS))
.chain(format_filters())
.chain(std::iter::once(("Swatch libraries", super::swatchlib::LIBRARY_EXTS)))
.chain(std::iter::once(("Flattener presets", super::flatten::PRESET_EXTS)))
.chain(std::iter::once(("PDF presets", super::pdfcmds::PRESET_EXTS)))
}
/// File → Place dialog filters: "All placeable files", then one per readable format, then text.
+17 -8
View File
@@ -24,7 +24,12 @@ pub(super) const OPTIONS: &[FormatOption] = &[
super::ARTBOARDS,
super::RANGE,
super::USE_ARTBOARDS,
FormatOption { name: "preset", ty: "string", default: "\"VectorCraft Default\"", description: "the PDF preset the other options apply over" },
FormatOption {
name: "preset",
ty: "string",
default: "\"VectorCraft Default\"",
description: "the PDF preset the other options apply over (built-in or saved: pdf.preset.list)",
},
FormatOption { name: "standard", ty: "string", default: "\"none\"", description: "none | pdfA2b (PDF/X is not supported yet)" },
FormatOption { name: "compatibility", ty: "string", default: "\"1.7\"", description: "PDF version: 1.4 | 1.5 | 1.6 | 1.7 | 2.0" },
FormatOption {
@@ -48,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Export PDF",
[],
None,
"{path?, preset?: \"VectorCraft Default\", artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a|pdfX3|pdfX4, compatibility?: 1.4|1.5|1.6|1.7 (default)|2.0, preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, PDF/A refuses it), thumbnails?, fastWebView?, viewAfterSaving? (the app opens the written file), createLayers?, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength}, compressText?: true}, marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6}, bleed?: {useDocument, top, bottom, left, right} (pt), output?: {conversion: none|destination|preserveNumbers, destination, profiles: none|all|destination|taggedSource, outputIntent, outputCondition, outputConditionId, registry, trapped}, advanced?: {fontSubsetPercent: 100, outlineText: true, overprint: preserve|discard}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options accepted but not applied yet come back as warnings; PDF/X, passwords and PDF/A-2b at 2.0 are refused. document.export {format: pdf} takes the same options",
"{path?, preset?: \"VectorCraft Default\" (built-in or saved: pdf.preset.list), artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a|pdfX3|pdfX4, compatibility?: 1.4|1.5|1.6|1.7 (default)|2.0, preserveEditing? (on in VectorCraft Default: the native document as an embedded file, which document.open restores; choosing a standard turns it off, PDF/A refuses it), thumbnails?, fastWebView?, viewAfterSaving? (the app opens the written file), createLayers?, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength}, compressText?: true}, marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6}, bleed?: {useDocument, top, bottom, left, right} (pt), output?: {conversion: none|destination|preserveNumbers, destination, profiles: none|all|destination|taggedSource, outputIntent, outputCondition, outputConditionId, registry, trapped}, advanced?: {fontSubsetPercent: 100, outlineText: true, overprint: preserve|discard}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options accepted but not applied yet come back as warnings; PDF/X, passwords and PDF/A-2b at 2.0 are refused. document.export {format: pdf} takes the same options",
has_doc,
export_pdf
),
@@ -57,7 +62,7 @@ pub fn specs() -> Vec<CommandSpec> {
"PDF Settings",
[],
None,
"{preset?, includeDocument?: false, …document.exportPdf options} → {settings (the preset with the options applied), presets: [name…], changed: [{option: \"compression.compressText\", value}] (what differs from the defaults), warnings}; includeDocument also exports the active document in memory and adds its warnings (pattern strokes, effects left out…)",
"{preset?, includeDocument?: false, …document.exportPdf options} → {settings (the preset with the options applied), presets: [name…], changed: [{option: \"compression.compressText\", value}] (what differs from the preset), warnings}; includeDocument also exports the active document in memory and adds its warnings (pattern strokes, effects left out…)",
always,
pdf_settings
),
@@ -66,7 +71,12 @@ pub fn specs() -> Vec<CommandSpec> {
/// Every preset name: the built-in ones, then the saved ones ([`crate::Prefs::pdf_presets`]).
pub fn presets(s: &Session) -> Vec<String> {
vectorcraft_pdf::builtin_presets().into_iter().map(|p| p.name).chain(s.prefs.pdf_presets.iter().map(|p| p.name.clone())).collect()
names(&s.prefs.pdf_presets)
}
/// The built-in preset names, then those of `saved`.
fn names(saved: &[PdfPreset]) -> Vec<String> {
vectorcraft_pdf::builtin_presets().into_iter().map(|p| p.name).chain(saved.iter().map(|p| p.name.clone())).collect()
}
/// The preset `name` names: a built-in one (any case; `default` is the app default) or one of
@@ -76,10 +86,9 @@ pub fn find_preset(name: &str, saved: &[PdfPreset]) -> Option<PdfPreset> {
}
fn preset(cmd: &str, name: &str, saved: &[PdfPreset]) -> Result<PdfSettings> {
find_preset(name, saved).map(|p| p.settings).ok_or_else(|| {
let names: Vec<String> = vectorcraft_pdf::builtin_presets().into_iter().map(|p| p.name).chain(saved.iter().map(|p| p.name.clone())).collect();
bad(cmd, format!("unknown PDF preset `{name}` (presets: {})", names.join(", ")))
})
find_preset(name, saved)
.map(|p| p.settings)
.ok_or_else(|| bad(cmd, format!("unknown PDF preset `{name}` (presets: {})", names(saved).join(", "))))
}
/// The settings of the preset `p` names (default: [`DEFAULT_PRESET`]), before `p`'s options.
+1 -1
View File
@@ -146,7 +146,7 @@ fn open_exts_cover_every_readable_format() {
}
let filters: Vec<_> = open_filters().collect();
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
assert_eq!(filters.len(), 3 + FORMATS.iter().filter(|f| f.read).count(), "and swatch libraries and flattener presets");
assert_eq!(filters.len(), 4 + FORMATS.iter().filter(|f| f.read).count(), "and swatch libraries, flattener presets and PDF presets");
}
#[test]
+2
View File
@@ -40,6 +40,7 @@ mod panelcmds;
mod path;
mod pathops;
mod patterncmds;
pub mod pdfcmds;
mod place;
pub mod prefscmds;
pub mod rasterfx;
@@ -220,6 +221,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(newdoc::specs());
v.extend(place::specs());
v.extend(links::specs());
v.extend(pdfcmds::specs());
v
})
}
+210
View File
@@ -0,0 +1,210 @@
//! Edit → PDF Presets: `pdf.preset.list`, `save`, `delete`, `export` and `import`. The built-in
//! presets are generated in code ([`vectorcraft_pdf::builtin_presets`]) and read-only; the user's
//! live in [`crate::Prefs::pdf_presets`]. Any of their names works as `preset` wherever PDF
//! options are taken (`document.exportPdf`, `document.export`, a `.ai` save…).
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use vectorcraft_pdf::{PdfPreset, builtin_preset, builtin_presets};
use super::fileio::pdf::{find_preset, resolve};
use super::*;
/// The `format` of a PDF presets file, also its extension.
pub const PRESET_FORMAT: &str = "vcpdfpresets";
/// The extensions of PDF presets files (opening one imports it).
pub const PRESET_EXTS: &[&str] = &[PRESET_FORMAT];
/// What `pdf.preset.export` writes and `pdf.preset.import` reads.
#[derive(Serialize, Deserialize)]
struct PresetFile {
format: String,
#[serde(default)]
presets: Vec<PdfPreset>,
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
query "pdf.preset.list",
"PDF Presets",
[],
None,
"{} → {presets: [{name, builtIn, supported (the writer produces its standard), description, settings (as document.pdfSettings)}]} the built-in presets (VectorCraft Default first), then the saved ones; any of these names works as `preset` wherever PDF options are taken",
always,
list
),
cmd!(
query "pdf.preset.save",
"Save PDF Preset",
[],
None,
"{name?: (default: a new \"PDF Preset N\"), newName?: rename it, description?, preset?: the preset to start from (default: the saved preset `name`, else VectorCraft Default), …document.exportPdf options} create or change a saved preset (built-in presets are read-only; passwords are never stored) → {name, created, settings}",
always,
save
),
cmd!(query "pdf.preset.delete", "Delete PDF Preset", [], None, "{name} delete a saved preset (built-in ones stay) → {deleted: name}", always, delete),
cmd!(
query "pdf.preset.export",
"Export PDF Presets",
[],
None,
"{names?: [preset names, built-in ones too] (default: every saved preset), path?} write the presets as a .vcpdfpresets file (JSON) → {path, count}; without path → {data: the file's text, count}",
always,
export
),
cmd!(
query "pdf.preset.import",
"Import PDF Presets",
[],
None,
"{path? | data?: file text | dataBase64?, replace?: false (replace saved presets of the same names; else imported ones whose name is taken get a number)} add the presets of a .vcpdfpresets file (as pdf.preset.export writes) to the saved ones → {imported: [names]}",
always,
import
),
]
}
impl Session {
/// Every PDF preset: the built-in ones, then the saved ones.
pub fn pdf_presets(&self) -> Vec<PdfPreset> {
let mut v = builtin_presets();
v.extend(self.prefs.pdf_presets.iter().cloned());
v
}
/// The first free "PDF Preset N": the name a new preset gets.
pub fn new_pdf_preset_name(&self) -> String {
(1..).map(|i| format!("PDF Preset {i}")).find(|n| !self.pdf_preset_taken(n, None)).unwrap_or_default()
}
/// The index of the saved PDF preset `name` (any case).
fn pdf_saved_index(&self, name: &str) -> Option<usize> {
self.prefs.pdf_presets.iter().position(|q| q.name.eq_ignore_ascii_case(name.trim()))
}
/// Whether `name` is taken by a built-in PDF preset or a saved one other than the one at
/// `except`.
fn pdf_preset_taken(&self, name: &str, except: Option<usize>) -> bool {
builtin_preset(name).is_some() || self.pdf_saved_index(name).is_some_and(|i| Some(i) != except)
}
}
fn row(p: PdfPreset, built_in: bool) -> Value {
json!({ "name": p.name, "builtIn": built_in, "supported": p.settings.check().is_ok(), "description": p.description, "settings": p.settings })
}
fn list(s: &mut Session, _: &Value) -> Result<Value> {
let rows = builtin_presets().into_iter().map(|p| row(p, true)).chain(s.prefs.pdf_presets.iter().cloned().map(|p| row(p, false)));
Ok(json!({ "presets": rows.collect::<Vec<_>>() }))
}
fn save(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "pdf.preset.save";
let name = match str_param(p, "name").map(str::trim) {
Some("") => return Err(bad(C, "`name` is empty")),
Some(n) => n.to_string(),
None => s.new_pdf_preset_name(),
};
if builtin_preset(&name).is_some() {
return Err(bad(C, format!("`{name}` is a built-in preset and is read-only: save it under another name")));
}
let at = s.pdf_saved_index(&name);
let saved = at.and_then(|i| s.prefs.pdf_presets.get(i)).cloned();
// A saved preset changes from its own settings unless `preset` says where to start.
let mut q = if p.is_object() { p.clone() } else { json!({}) };
if let (Some(old), None) = (&saved, str_param(p, "preset")) {
q["preset"] = json!(old.name);
}
let mut settings = resolve(C, &q, &s.prefs.pdf_presets)?;
settings.clear_passwords();
let name = match str_param(p, "newName").map(str::trim) {
Some("") => return Err(bad(C, "`newName` is empty")),
Some(n) if s.pdf_preset_taken(n, at) => return Err(bad(C, format!("a preset named `{n}` exists"))),
Some(n) => n.to_string(),
None => saved.as_ref().map_or(name, |old| old.name.clone()),
};
let description = str_param(p, "description").map(str::to_string).or_else(|| saved.map(|old| old.description)).unwrap_or_default();
let preset = PdfPreset { name: name.clone(), description, settings: settings.clone() };
match at.and_then(|i| s.prefs.pdf_presets.get_mut(i)) {
Some(slot) => *slot = preset,
None => s.prefs.pdf_presets.push(preset),
}
Ok(json!({ "name": name, "created": at.is_none(), "settings": settings }))
}
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "pdf.preset.delete";
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?;
if builtin_preset(name).is_some() {
return Err(bad(C, format!("`{name}` is a built-in preset and stays")));
}
let i = s.pdf_saved_index(name).ok_or_else(|| bad(C, format!("no saved PDF preset named `{name}`")))?;
Ok(json!({ "deleted": s.prefs.pdf_presets.remove(i).name }))
}
fn export(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "pdf.preset.export";
let presets = match p.get("names").and_then(Value::as_array) {
Some(names) => {
let find = |n: &Value| {
let n = n.as_str().unwrap_or_default();
find_preset(n, &s.prefs.pdf_presets).ok_or_else(|| bad(C, format!("no PDF preset named `{n}`")))
};
names.iter().map(find).collect::<Result<Vec<_>>>()?
}
None => s.prefs.pdf_presets.clone(),
};
if presets.is_empty() {
return Err(bad(C, "no saved presets to export (name built-in ones in `names`)"));
}
let count = presets.len();
let text = serde_json::to_string_pretty(&PresetFile { format: PRESET_FORMAT.into(), presets }).map_err(|e| bad(C, e.to_string()))?;
match str_param(p, "path") {
Some(path) => {
super::fileio::write_file(path, text.as_bytes())?;
Ok(json!({"path": path, "count": count}))
}
None => Ok(json!({"data": text, "count": count})),
}
}
fn import(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "pdf.preset.import";
let bytes = match (str_param(p, "path"), str_param(p, "data"), str_param(p, "dataBase64")) {
(Some(path), ..) => super::fileio::read_file(path)?,
(None, Some(text), _) => text.as_bytes().to_vec(),
(None, None, Some(b64)) => vectorcraft_format::base64_decode(b64).ok_or_else(|| bad(C, "bad dataBase64"))?,
_ => return Err(bad(C, "give `path`, `data` or `dataBase64`")),
};
let file: PresetFile = serde_json::from_slice(&bytes).map_err(|e| bad(C, format!("not a PDF presets file: {e}")))?;
if file.format != PRESET_FORMAT {
return Err(bad(C, format!("not a PDF presets file (format `{}`)", file.format)));
}
// Values a hand-edited file may carry are refused as the export options refuse them.
for q in &file.presets {
q.settings.check_values().map_err(|e| bad(C, format!("`{}`: {e}", q.name)))?;
}
let replace = bool_or(p, "replace", false);
let mut imported = vec![];
for mut preset in file.presets {
preset.settings.clear_passwords();
let base = Some(preset.name.trim()).filter(|n| !n.is_empty()).unwrap_or("PDF Preset").to_string();
match s.pdf_saved_index(&base).filter(|_| replace) {
Some(i) => {
if let Some(slot) = s.prefs.pdf_presets.get_mut(i) {
preset.name = slot.name.clone();
imported.push(preset.name.clone());
*slot = preset;
}
}
None => {
preset.name = unique_name(&base, |n| s.pdf_preset_taken(n, None));
imported.push(preset.name.clone());
s.prefs.pdf_presets.push(preset);
}
}
}
Ok(json!({ "imported": imported }))
}
+2
View File
@@ -996,6 +996,8 @@ mod tests_pathops;
#[cfg(test)]
mod tests_pattern;
#[cfg(test)]
mod tests_pdfpresets;
#[cfg(test)]
mod tests_place;
#[cfg(test)]
mod tests_prefs;
+137
View File
@@ -0,0 +1,137 @@
//! Edit → PDF Presets: built-in presets are read-only, saved ones apply wherever a preset is named,
//! and presets files round-trip.
use serde_json::{Value, json};
use crate::Session;
use crate::cmd::fileio::pdf::DEFAULT_PRESET;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
s
}
fn names(s: &mut Session) -> Vec<String> {
let v = s.execute("pdf.preset.list", &json!({})).unwrap();
v["presets"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap().to_string()).collect()
}
fn pdf(v: &Value) -> Vec<u8> {
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
}
fn is_bad_params(r: crate::Result<Value>) -> bool {
matches!(r, Err(crate::EngineError::BadParams { .. }))
}
#[test]
fn built_in_presets_are_listed_and_read_only() {
let mut s = session();
let v = s.execute("pdf.preset.list", &json!({})).unwrap();
let rows = v["presets"].as_array().unwrap();
assert_eq!(rows[0]["name"], DEFAULT_PRESET);
assert!(rows.iter().all(|r| r["builtIn"] == true && !r["description"].as_str().unwrap().is_empty()));
let row = |n: &str| rows.iter().find(|r| r["name"] == n).unwrap_or_else(|| panic!("{n}"));
for n in ["High Quality Print", "Press Quality", "Smallest File Size", "PDF/X-1a:2001", "PDF/X-3:2002", "PDF/X-4:2010"] {
assert_eq!(row(n)["builtIn"], true);
}
assert_eq!(row("PDF/X-4:2010")["supported"], false, "PDF/X is not written yet");
assert_eq!(row(DEFAULT_PRESET)["settings"]["preserveEditing"], true);
// Read-only: neither changed nor deleted, in any case.
assert!(is_bad_params(s.execute("pdf.preset.save", &json!({"name": "press quality", "compatibility": "1.4"}))));
assert!(is_bad_params(s.execute("pdf.preset.save", &json!({"name": "default"}))));
assert!(is_bad_params(s.execute("pdf.preset.delete", &json!({"name": "Smallest File Size"}))));
assert_eq!(names(&mut s).len(), 7);
// Built-in presets apply as named.
let v = s.execute("document.pdfSettings", &json!({"preset": "Smallest File Size"})).unwrap();
assert_eq!((&v["settings"]["fastWebView"], &v["changed"]), (&json!(true), &json!([])), "{v}");
assert!(is_bad_params(s.execute("document.exportPdf", &json!({"preset": "PDF/X-4:2010"}))), "PDF/X is refused on export");
}
#[test]
fn saved_presets_apply_wherever_a_preset_is_named() {
let mut s = session();
let r = s
.execute("pdf.preset.save", &json!({"name": "Web", "preset": "Smallest File Size", "compatibility": "1.5", "description": "For the site"}))
.unwrap();
assert_eq!((&r["name"], &r["created"]), (&json!("Web"), &json!(true)));
assert_eq!(names(&mut s).last().map(String::as_str), Some("Web"));
// document.exportPdf, document.export, serialize and pdfSettings take it.
let v = s.execute("document.exportPdf", &json!({"preset": "web"})).unwrap();
assert!(pdf(&v).starts_with(b"%PDF-1.5"), "the preset's compatibility");
assert!(v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("fast web view")), "{v}");
let v = s.execute("document.export", &json!({"format": "pdf", "preset": "Web"})).unwrap();
assert!(pdf(&v).starts_with(b"%PDF-1.5"));
let v = s.execute("document.serialize", &json!({"format": "pdf", "preset": "Web", "compatibility": "1.6"})).unwrap();
assert!(pdf(&v).starts_with(b"%PDF-1.6"), "options apply over it");
let v = s.execute("document.pdfSettings", &json!({"preset": "Web", "thumbnails": true})).unwrap();
assert_eq!(v["changed"], json!([{"option": "thumbnails", "value": true}]), "{v}");
assert!(v["presets"].as_array().unwrap().contains(&json!("Web")));
// Saving it again changes it from its own settings; it can be renamed but not to a taken name.
let r = s.execute("pdf.preset.save", &json!({"name": "Web", "thumbnails": true})).unwrap();
assert_eq!((&r["created"], &r["settings"]["compatibility"], &r["settings"]["thumbnails"]), (&json!(false), &json!("1.5"), &json!(true)));
assert!(is_bad_params(s.execute("pdf.preset.save", &json!({"name": "Web", "newName": "Press Quality"}))));
assert_eq!(s.execute("pdf.preset.save", &json!({"name": "Web", "newName": "Site"})).unwrap()["name"], "Site");
assert_eq!(s.prefs.pdf_presets[0].description, "For the site", "kept");
// Passwords are never stored; new presets get a free name.
s.execute("pdf.preset.save", &json!({"name": "Locked", "security": {"openPassword": "x"}})).unwrap();
assert!(s.prefs.pdf_presets[1].settings.security.open_password.is_empty());
assert_eq!(s.execute("pdf.preset.save", &json!({})).unwrap()["name"], "PDF Preset 1");
assert_eq!(s.execute("pdf.preset.delete", &json!({"name": "locked"})).unwrap()["deleted"], "Locked");
assert!(is_bad_params(s.execute("document.exportPdf", &json!({"preset": "Locked"}))), "deleted");
}
#[test]
fn presets_export_and_import_round_trip() {
let mut s = session();
s.execute("pdf.preset.save", &json!({"name": "Proof", "preset": "High Quality Print", "marks": {"trim": true}})).unwrap();
let saved = s.prefs.pdf_presets.clone();
let data = s.execute("pdf.preset.export", &json!({"names": ["Proof", "Press Quality"]})).unwrap();
assert_eq!(data["count"], 2);
let text = data["data"].as_str().unwrap().to_string();
s.execute("pdf.preset.delete", &json!({"name": "Proof"})).unwrap();
let r = s.execute("pdf.preset.import", &json!({"data": text})).unwrap();
assert_eq!(r["imported"], json!(["Proof", "Press Quality 2"]), "a built-in name gets a number");
assert_eq!(s.prefs.pdf_presets[0], saved[0], "the same preset comes back");
let press = vectorcraft_pdf::builtin_preset("Press Quality").unwrap();
assert_eq!(s.prefs.pdf_presets[1].settings, press.settings);
// replace: saved presets of the same names are overwritten.
let r = s.execute("pdf.preset.import", &json!({"data": text, "replace": true})).unwrap();
assert_eq!(r["imported"], json!(["Proof", "Press Quality 3"]));
assert_eq!(s.prefs.pdf_presets.len(), 3);
// Through a file, and every saved preset by default.
let path = std::env::temp_dir().join(format!("vc-pdfpresets-{}.vcpdfpresets", std::process::id())).to_string_lossy().to_string();
assert_eq!(s.execute("pdf.preset.export", &json!({"path": path})).unwrap()["count"], 3);
let mut t = session();
assert_eq!(t.execute("pdf.preset.import", &json!({"path": path})).unwrap()["imported"].as_array().unwrap().len(), 3);
assert_eq!(t.prefs.pdf_presets, s.prefs.pdf_presets);
let _ = std::fs::remove_file(&path);
// Not a presets file, or values out of range: refused, nothing added.
for bad in [
json!({"data": "{}"}),
json!({"data": "{\"format\": \"vcflattener\", \"presets\": []}"}),
json!({"data": "{\"format\": \"vcpdfpresets\", \"presets\": [{\"name\": \"X\", \"settings\": {\"marks\": {\"weight\": 99}}}]}"}),
json!({"dataBase64": "!!"}),
json!({}),
] {
assert!(is_bad_params(t.execute("pdf.preset.import", &bad)), "{bad}");
}
assert_eq!(t.prefs.pdf_presets.len(), 3);
assert!(is_bad_params(Session::new().execute("pdf.preset.export", &json!({}))), "nothing saved to export");
}
#[test]
fn saved_presets_persist_with_the_preferences() {
let mut s = session();
s.execute("pdf.preset.save", &json!({"name": "Kept", "compatibility": "1.4"})).unwrap();
let back: crate::Prefs = serde_json::from_value(s.prefs.to_json()).unwrap();
assert_eq!(back.pdf_presets, s.prefs.pdf_presets);
// Resetting the preferences keeps them (a library, not a preference).
s.execute("prefs.reset", &json!({})).unwrap();
assert_eq!(s.prefs.pdf_presets.len(), 1);
// Preferences saved before PDF presets existed still load.
let old: crate::Prefs = serde_json::from_value(json!({"keyboardIncrement": 2.0})).unwrap();
assert!(old.pdf_presets.is_empty());
}