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:
parent
0ffaa427d1
commit
9aa29aa0fd
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -563,3 +563,30 @@ proptest! {
|
|||
survive("mutated ai", || vectorcraft_engine::cmd::fileio::load("saved.ai", &b).ok().map(|l| l.doc))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- PDF presets files ----------
|
||||
|
||||
/// Import PDF presets from `data`, then export with each one imported (and save the .ai).
|
||||
fn pdf_presets(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
survive_library(what, "pdf.preset.import", data, |s, r| {
|
||||
for name in r["imported"].as_array().into_iter().flatten() {
|
||||
let _ = s.execute("document.exportPdf", &json!({"preset": name}));
|
||||
let _ = s.execute("document.save", &json!({"format": "ai", "preset": name}));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
#[test]
|
||||
fn pdf_presets_garbage_never_panics(s in ".{0,300}", head in prop::sample::select(vec!["", "{\"format\": \"vcpdfpresets\", ", "{\"format\": \"vcpdfpresets\", \"presets\": [{\"name\": \"x\", \"settings\": "])) {
|
||||
pdf_presets("pdf presets garbage", &format!("{head}{s}"))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_pdf_presets_never_panic(cut in 0usize..20_000, edits in prop::collection::vec(arb_edit(), 0..10)) {
|
||||
let text = saved("pdf.preset.export", json!({"names": ["VectorCraft Default", "Smallest File Size", "PDF/X-4:2010"]}));
|
||||
pdf_presets("mutated pdf presets", &mutate_text(&text, cut, &edits))?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ mod tests_import_color;
|
|||
#[cfg(test)]
|
||||
mod tests_import_options;
|
||||
#[cfg(test)]
|
||||
mod tests_presets;
|
||||
#[cfg(test)]
|
||||
mod tests_settings;
|
||||
#[cfg(test)]
|
||||
mod tests_stroke;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
//! PDF presets: named [`PdfSettings`]. The built-in ones are generated here (the app default
|
||||
//! keeps the document editable).
|
||||
//! PDF presets: named [`PdfSettings`]. The built-in ones are generated here (the app default,
|
||||
//! which keeps the document editable, plus presets for print, press, small files and the PDF/X
|
||||
//! standards); the user's own live in the app's preferences.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::PdfSettings;
|
||||
use crate::{
|
||||
BleedSettings, Compatibility, CompressionSettings, Downsample, ImageCodec, ImageCompression, JpegQuality, MonoCodec, MonoCompression,
|
||||
PdfSettings, Standard,
|
||||
};
|
||||
|
||||
/// The built-in preset every PDF export starts from.
|
||||
pub const DEFAULT_PRESET: &str = "VectorCraft Default";
|
||||
|
|
@ -20,13 +24,75 @@ pub struct PdfPreset {
|
|||
pub settings: PdfSettings,
|
||||
}
|
||||
|
||||
/// Colour and greyscale images resampled (bicubic) to `ppi` above `above` ppi, compressed with
|
||||
/// `codec`.
|
||||
fn images(ppi: f64, above: f64, codec: ImageCodec, quality: JpegQuality) -> ImageCompression {
|
||||
ImageCompression { downsample: Downsample::Bicubic, ppi, above_ppi: above, compression: codec, quality }
|
||||
}
|
||||
|
||||
/// Compression with the same settings for colour and greyscale images, and CCITT Group 4
|
||||
/// monochrome images resampled to `mono_ppi` above 1.5 × that.
|
||||
fn compression(img: ImageCompression, mono_ppi: f64) -> CompressionSettings {
|
||||
let mono = MonoCompression { downsample: Downsample::Bicubic, ppi: mono_ppi, above_ppi: mono_ppi * 1.5, compression: MonoCodec::CcittG4 };
|
||||
CompressionSettings { color: img.clone(), gray: img, mono, compress_text: true }
|
||||
}
|
||||
|
||||
/// Settings for print: PDF 1.6, images kept at 300 ppi, the document's bleed when `bleed`.
|
||||
fn print(codec: ImageCodec, bleed: bool) -> PdfSettings {
|
||||
PdfSettings {
|
||||
compatibility: Compatibility::Pdf16,
|
||||
compression: compression(images(300.0, 450.0, codec, JpegQuality::Maximum), 1200.0),
|
||||
bleed: BleedSettings { use_document: bleed, ..Default::default() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A PDF/X preset: print settings for `standard` at PDF version `compatibility`.
|
||||
fn pdf_x(standard: Standard, compatibility: Compatibility) -> PdfSettings {
|
||||
PdfSettings { standard, compatibility, ..print(ImageCodec::Auto, true) }
|
||||
}
|
||||
|
||||
/// Every built-in preset, the app default first.
|
||||
pub fn builtin_presets() -> Vec<PdfPreset> {
|
||||
vec![PdfPreset {
|
||||
name: DEFAULT_PRESET.into(),
|
||||
description: "Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again.".into(),
|
||||
settings: PdfSettings { preserve_editing: true, ..Default::default() },
|
||||
}]
|
||||
let preset = |name: &str, description: &str, settings| PdfPreset { name: name.into(), description: description.into(), settings };
|
||||
vec![
|
||||
preset(
|
||||
DEFAULT_PRESET,
|
||||
"Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again.",
|
||||
PdfSettings { preserve_editing: true, ..Default::default() },
|
||||
),
|
||||
preset("High Quality Print", "For desktop printers and proofs: images stay at print resolution.", print(ImageCodec::Auto, false)),
|
||||
preset(
|
||||
"Press Quality",
|
||||
"For commercial printing: high-resolution images compressed losslessly, the document's bleed.",
|
||||
print(ImageCodec::Zip, true),
|
||||
),
|
||||
preset(
|
||||
"Smallest File Size",
|
||||
"For screens, email and the web: images resampled to 100 ppi and compressed.",
|
||||
PdfSettings {
|
||||
compatibility: Compatibility::Pdf16,
|
||||
fast_web_view: true,
|
||||
compression: compression(images(100.0, 150.0, ImageCodec::Jpeg, JpegQuality::Medium), 300.0),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
preset(
|
||||
"PDF/X-1a:2001",
|
||||
"Print exchange with CMYK and spot colours only (PDF/X output is not supported yet).",
|
||||
pdf_x(Standard::PdfX1a, Compatibility::Pdf14),
|
||||
),
|
||||
preset(
|
||||
"PDF/X-3:2002",
|
||||
"Print exchange allowing colour-managed colours (PDF/X output is not supported yet).",
|
||||
pdf_x(Standard::PdfX3, Compatibility::Pdf14),
|
||||
),
|
||||
preset(
|
||||
"PDF/X-4:2010",
|
||||
"Print exchange keeping live transparency and layers (PDF/X output is not supported yet).",
|
||||
pdf_x(Standard::PdfX4, Compatibility::Pdf16),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// The built-in preset `name` names (any case; `default` is the app default).
|
||||
|
|
|
|||
|
|
@ -424,6 +424,12 @@ impl PdfSettings {
|
|||
within("advanced.fontSubsetPercent", self.advanced.font_subset_percent, 0.0, 100.0, "%")
|
||||
}
|
||||
|
||||
/// Forget the passwords (presets never store them).
|
||||
pub fn clear_passwords(&mut self) {
|
||||
self.security.open_password.clear();
|
||||
self.security.permissions_password.clear();
|
||||
}
|
||||
|
||||
/// Options that are accepted but not applied by the writer yet, one warning each.
|
||||
pub fn warnings(&self) -> Vec<String> {
|
||||
let d = Self::default();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
//! The built-in PDF presets: generated in code, valid, described, and round-tripping as JSON.
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn built_in_presets() {
|
||||
let all = builtin_presets();
|
||||
assert_eq!(all[0].name, DEFAULT_PRESET);
|
||||
assert!(all[0].settings.preserve_editing, "the app default keeps the document editable");
|
||||
assert_eq!(all.len(), 7);
|
||||
for p in &all {
|
||||
assert_eq!(p.settings.check_values(), Ok(()), "{}", p.name);
|
||||
assert!(!p.description.is_empty());
|
||||
let back: PdfPreset = serde_json::from_value(serde_json::to_value(p).unwrap()).unwrap();
|
||||
assert_eq!(&back, p, "round-trips");
|
||||
}
|
||||
// PDF/X presets name standards the writer doesn't produce yet.
|
||||
let x4 = builtin_preset("pdf/x-4:2010").unwrap();
|
||||
assert!(matches!(x4.settings.check(), Err(PdfError::Unsupported(_))));
|
||||
assert_eq!(builtin_preset("default").unwrap().name, DEFAULT_PRESET);
|
||||
assert!(builtin_preset("Nope").is_none());
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ pub mod new_color_group;
|
|||
mod new_document;
|
||||
pub mod new_swatch;
|
||||
mod path_ops;
|
||||
pub mod pdf_presets;
|
||||
pub mod place;
|
||||
mod png_options;
|
||||
pub mod raster_effects;
|
||||
|
|
@ -61,7 +62,7 @@ pub use effect::open as open_effect_dialog;
|
|||
pub use export_as::open as open_export_as;
|
||||
pub use new_document::{open as open_new_document, preset_card};
|
||||
pub use png_options::open as open_raster_options;
|
||||
pub use save_pdf::open as open_save_pdf;
|
||||
pub use save_pdf::{open as open_save_pdf, open_preset as open_pdf_preset};
|
||||
pub use tools::open_tool_dialog;
|
||||
|
||||
use crate::state::Dialog;
|
||||
|
|
@ -198,6 +199,8 @@ registry! {
|
|||
RasterEffectsSettings: [raster_effects::KIND] => raster_effects::SPEC,
|
||||
MissingLinks: [missing_links::KIND] => missing_links::SPEC,
|
||||
TextImport: [text_import::KIND] => text_import::SPEC,
|
||||
PdfPresets: [pdf_presets::KIND] => pdf_presets::SPEC,
|
||||
PdfPreset: [save_pdf::PRESET_KIND] => save_pdf::PRESET_SPEC,
|
||||
}
|
||||
|
||||
/// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
//! Edit → PDF Presets: every preset in a list (built-in ones in brackets, read only) with the
|
||||
//! selected one's description and how its settings differ from the app default beside it; New…
|
||||
//! (a preset starting from the selected one), Edit… (saved presets), Delete, Import… and Export…
|
||||
//! (the selected preset) under the list. New… and Edit… open the preset editor (the Save PDF
|
||||
//! dialog, kind `pdfPreset`), whose OK comes back here.
|
||||
//!
|
||||
//! Field: `selected` (a preset's name).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::cmd::fileio::pdf;
|
||||
use vectorcraft_engine::cmd::pdfcmds::PRESET_FORMAT;
|
||||
|
||||
use super::DialogSpec;
|
||||
use super::save_pdf::{open_preset, option_label, section_of};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
/// The dialog kind of PDF Presets.
|
||||
pub const KIND: &str = "pdfPresets";
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |_| "PDF Presets".into(),
|
||||
body,
|
||||
confirm: |app, _| {
|
||||
app.ui.dialog = None;
|
||||
Ok(Value::Null)
|
||||
},
|
||||
ok: None,
|
||||
min_width: 680.0,
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// Open the presets manager on `selected` (default: the app default).
|
||||
pub fn open(app: &mut VectorcraftApp, selected: Option<&str>) {
|
||||
app.ui.dialog = Some(Dialog::new(KIND, json!({ "selected": selected.unwrap_or(pdf::DEFAULT_PRESET) })));
|
||||
}
|
||||
|
||||
/// What a button asked for, done once the list and the details are drawn.
|
||||
enum Action {
|
||||
Select(String),
|
||||
New,
|
||||
Edit,
|
||||
Delete,
|
||||
Import,
|
||||
Export,
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let presets = app.session.pdf_presets();
|
||||
let builtins = presets.len() - app.session.prefs.pdf_presets.len();
|
||||
let i = presets.iter().position(|p| p.name.eq_ignore_ascii_case(d.str("selected").trim())).unwrap_or(0);
|
||||
let Some(preset) = presets.get(i) else { return false };
|
||||
let builtin = i < builtins;
|
||||
let mut act = None;
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(300.0);
|
||||
widgets::dim_label(ui, "Presets:");
|
||||
widgets::list_box(ui, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("pdf-presets").min_scrolled_height(260.0).max_height(260.0).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.set_min_height(260.0);
|
||||
for (k, p) in presets.iter().enumerate() {
|
||||
let label = if k < builtins { format!("[{}]", p.name) } else { p.name.clone() };
|
||||
if ui.selectable_label(k == i, label).clicked() {
|
||||
act = Some(Action::Select(p.name.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() {
|
||||
act = Some(Action::New);
|
||||
}
|
||||
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() {
|
||||
act = Some(Action::Edit);
|
||||
}
|
||||
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() {
|
||||
act = Some(Action::Delete);
|
||||
}
|
||||
if widgets::flat_button(ui, "Import…", 64.0).clicked() {
|
||||
act = Some(Action::Import);
|
||||
}
|
||||
if widgets::flat_button(ui, "Export…", 64.0).on_hover_text("Save the selected preset to a file").clicked() {
|
||||
act = Some(Action::Export);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(18.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(340.0);
|
||||
ui.label(egui::RichText::new(&preset.name).color(t.text_strong).strong());
|
||||
if !preset.description.is_empty() {
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new(&preset.description).color(t.text));
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
details(ui, &t, preset, builtin);
|
||||
});
|
||||
});
|
||||
if let Some(a) = act
|
||||
&& let Err(e) = run(app, d, a, &preset.name)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// What sets the preset apart: its settings that differ from the app default, and the notes on
|
||||
/// what Save PDF can't honour or apply yet.
|
||||
fn details(ui: &mut egui::Ui, t: &Tokens, preset: &vectorcraft_pdf::PdfPreset, builtin: bool) {
|
||||
let dim = |ui: &mut egui::Ui, text: &str| ui.label(egui::RichText::new(text).color(t.text_dim).size(11.5));
|
||||
widgets::dim_label(ui, &format!("Differences from {}:", pdf::DEFAULT_PRESET));
|
||||
let base = vectorcraft_pdf::builtin_preset(pdf::DEFAULT_PRESET).map(|p| p.settings).unwrap_or_default();
|
||||
let mut changed = pdf::changes(&preset.settings, &base);
|
||||
changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default()));
|
||||
egui::ScrollArea::vertical().id_salt("pdf-preset-details").max_height(180.0).show(ui, |ui| {
|
||||
if changed.is_empty() {
|
||||
dim(ui, "None.");
|
||||
}
|
||||
for c in &changed {
|
||||
let value = match &c["value"] {
|
||||
Value::Bool(b) => if *b { "On" } else { "Off" }.to_string(),
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
ui.label(egui::RichText::new(format!("{}: {value}", option_label(c["option"].as_str().unwrap_or_default()))).color(t.text));
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
if let Err(e) = preset.settings.check() {
|
||||
dim(ui, &format!("⚠ {e}"));
|
||||
}
|
||||
for w in preset.settings.warnings() {
|
||||
dim(ui, &format!("⚠ {w}"));
|
||||
}
|
||||
if builtin {
|
||||
ui.add_space(6.0);
|
||||
dim(ui, "Built-in presets are read-only: New… starts an editable copy.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the preset `name`.
|
||||
fn select(d: &mut Dialog, name: &str) {
|
||||
d.fields.insert("selected".into(), json!(name));
|
||||
}
|
||||
|
||||
fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current: &str) -> Result<(), String> {
|
||||
match act {
|
||||
Action::Select(name) => select(d, &name),
|
||||
// The editor replaces this dialog; its OK comes back here.
|
||||
Action::New => {
|
||||
open_preset(app, &json!({ "preset": current }))?;
|
||||
*d = app.ui.dialog.take().ok_or("no preset editor")?;
|
||||
}
|
||||
Action::Edit => {
|
||||
open_preset(app, &json!({ "name": current }))?;
|
||||
*d = app.ui.dialog.take().ok_or("no preset editor")?;
|
||||
}
|
||||
Action::Delete => {
|
||||
let i = app.session.prefs.pdf_presets.iter().position(|p| p.name == current);
|
||||
app.run("pdf.preset.delete", json!({ "name": current }))?;
|
||||
// The one above it takes its place (the last built-in one above the first saved one).
|
||||
let builtins = vectorcraft_pdf::builtin_presets().len();
|
||||
let next = app.session.pdf_presets().get((i.unwrap_or(0) + builtins).saturating_sub(1)).map(|p| p.name.clone());
|
||||
select(d, &next.unwrap_or_default());
|
||||
}
|
||||
Action::Import => {
|
||||
// On the web the picked file arrives later and imports through `io::open_bytes`.
|
||||
if let Some(f) = app.services.open_async.as_mut() {
|
||||
f();
|
||||
return Ok(());
|
||||
}
|
||||
let path = app.services.pick_open.as_mut().and_then(|f| f()).ok_or("cancelled")?;
|
||||
let r = app.run("pdf.preset.import", json!({ "path": path }))?;
|
||||
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
|
||||
select(d, first);
|
||||
}
|
||||
}
|
||||
Action::Export => {
|
||||
crate::io::save_command_output(app, "pdf.preset.export", PRESET_FORMAT, json!({ "names": [current] }))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
fn frame(app: &mut VectorcraftApp) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
fn kind(app: &VectorcraftApp) -> Option<&str> {
|
||||
app.ui.dialog.as_ref().map(|d| d.kind.as_str())
|
||||
}
|
||||
|
||||
/// Press a manager button on the preset `current`, as the body does.
|
||||
fn press(app: &mut VectorcraftApp, act: Action, current: &str) {
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
run(app, &mut d, act, current).unwrap();
|
||||
app.ui.dialog = Some(d);
|
||||
frame(app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_edit_and_delete_go_through_the_preset_editor() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("ui.pdfPresetsDialog", json!({"selected": "Press Quality"})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(kind(&app), Some(KIND));
|
||||
// New… opens the editor on a copy of the selected preset; its OK saves it and comes back.
|
||||
press(&mut app, Action::New, "Press Quality");
|
||||
assert_eq!(kind(&app), Some("pdfPreset"));
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
assert_eq!((d.str("name"), d.fields["compatibility"].clone()), ("PDF Preset 1".to_string(), json!("1.6")));
|
||||
d.fields.insert("name".into(), json!("Press 1.5"));
|
||||
d.fields.insert("compatibility".into(), json!("1.5"));
|
||||
d.fields.insert("description".into(), json!("Mine"));
|
||||
for s in super::super::save_pdf::SECTIONS {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("__section".into(), json!(s));
|
||||
frame(&mut app);
|
||||
}
|
||||
let r = super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(r["name"], "Press 1.5");
|
||||
assert_eq!((kind(&app), app.ui.dialog.as_ref().unwrap().str("selected")), (Some(KIND), "Press 1.5".to_string()));
|
||||
let saved = &app.session.prefs.pdf_presets[0];
|
||||
assert_eq!((saved.settings.compatibility.id(), saved.description.as_str()), ("1.5", "Mine"));
|
||||
assert!(saved.settings.bleed.use_document, "the rest comes from Press Quality");
|
||||
frame(&mut app);
|
||||
// Edit… renames it; a new preset can't take a taken name.
|
||||
press(&mut app, Action::Edit, "Press 1.5");
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("__editing"), "Press 1.5");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Press"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.prefs.pdf_presets[0].name, "Press");
|
||||
press(&mut app, Action::New, "Press");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("press"));
|
||||
assert!(super::super::confirm(&mut app).is_err());
|
||||
assert_eq!(kind(&app), Some("pdfPreset"), "stays open on a taken name");
|
||||
// Built-in presets can't be edited; Delete selects the preset above.
|
||||
assert!(open_preset(&mut app, &json!({"name": "Press Quality"})).is_err());
|
||||
open(&mut app, Some("Press"));
|
||||
press(&mut app, Action::Delete, "Press");
|
||||
assert!(app.session.prefs.pdf_presets.is_empty());
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("selected"), "PDF/X-4:2010");
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "Close");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_pdf_saves_its_settings_as_a_preset() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
|
||||
app.run("ui.savePdfDialog", json!({"preset": "High Quality Print", "compatibility": "1.5"})).unwrap();
|
||||
frame(&mut app);
|
||||
// Save Preset… asks for a name; OK (or Enter) then saves the preset, not the PDF.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("__savePresetAs".into(), json!("Proofs"));
|
||||
frame(&mut app);
|
||||
let r = super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(r["name"], "Proofs");
|
||||
let d = app.ui.dialog.as_ref().expect("Save PDF stays open");
|
||||
assert_eq!((d.kind.as_str(), d.str("preset")), ("savePdf", "Proofs".to_string()));
|
||||
assert!(!d.fields.contains_key("__savePresetAs"));
|
||||
assert!(d.fields["__presets"].as_array().unwrap().contains(&json!("Proofs")));
|
||||
let saved = &app.session.prefs.pdf_presets[0].settings;
|
||||
assert_eq!(saved.compatibility.id(), "1.5");
|
||||
assert_eq!(
|
||||
saved,
|
||||
&vectorcraft_pdf::PdfSettings {
|
||||
compatibility: saved.compatibility,
|
||||
..vectorcraft_pdf::builtin_preset("High Quality Print").unwrap().settings
|
||||
}
|
||||
);
|
||||
// A built-in name is refused and the row stays.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("__savePresetAs".into(), json!("Press Quality"));
|
||||
assert!(super::super::confirm(&mut app).is_err());
|
||||
frame(&mut app);
|
||||
assert_eq!(app.session.prefs.pdf_presets.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_and_export_go_through_files() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-pdfpresets-ui-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("mine.vcpdfpresets").to_string_lossy().to_string();
|
||||
let picked = file.clone();
|
||||
let services = crate::Services {
|
||||
pick_save: Some(Box::new(move |_: &str| Some(picked.clone()))),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("pdf.preset.save", json!({"name": "Mine", "thumbnails": true})).unwrap();
|
||||
app.run("ui.pdfPresetsDialog", json!({"selected": "Mine"})).unwrap();
|
||||
frame(&mut app);
|
||||
press(&mut app, Action::Export, "Mine");
|
||||
// Opening the file imports it (also how the web's picked file arrives).
|
||||
let bytes = std::fs::read(&file).unwrap();
|
||||
crate::io::open_bytes(&mut app, &file, &bytes, Some(file.clone())).unwrap();
|
||||
let names: Vec<&str> = app.session.prefs.pdf_presets.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Mine", "Mine 2"]);
|
||||
assert!(app.session.prefs.pdf_presets[1].settings.thumbnails);
|
||||
assert!(app.ui.status.contains("PDF presets"), "{}", app.ui.status);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,10 @@
|
|||
//! The fields are the `document.exportPdf` options (sections are objects such as `compression`),
|
||||
//! plus `preset`, `range`, `path?` and UI-only `__` keys, so agents fill the dialog with
|
||||
//! `ui.dialog.set`. OK runs `document.exportPdf` through [`io::export_pdf`]; the Summary is
|
||||
//! `document.pdfSettings`.
|
||||
//! `document.pdfSettings`. Save Preset… names the settings as a preset (`pdf.preset.save`).
|
||||
//!
|
||||
//! The same dialog edits a preset (kind `pdfPreset`, from Edit → PDF Presets): a `name` and
|
||||
//! `description` instead of the preset and artboards, and OK saves the preset.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
|
|
@ -25,7 +28,23 @@ pub(super) const KIND: &str = "savePdf";
|
|||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Save PDF".into(), body, confirm, ok: Some("Save PDF"), ..DialogSpec::FORM };
|
||||
|
||||
const SECTIONS: [&str; 7] = ["General", "Compression", "Marks and Bleeds", "Output", "Advanced", "Security", "Summary"];
|
||||
/// The dialog kind of the preset editor (New / Edit in Edit → PDF Presets).
|
||||
pub(super) const PRESET_KIND: &str = "pdfPreset";
|
||||
|
||||
pub(super) const PRESET_SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| if d.str(EDITING).is_empty() { "New PDF Preset" } else { "Edit PDF Preset" }.into(),
|
||||
body,
|
||||
confirm: confirm_preset,
|
||||
ok: Some("Save Preset"),
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// The preset editor's field holding the name of the saved preset it edits (empty: a new one).
|
||||
const EDITING: &str = "__editing";
|
||||
/// Save PDF's field holding the name typed for Save Preset… (present while that row shows).
|
||||
const SAVE_AS: &str = "__savePresetAs";
|
||||
|
||||
pub(super) const SECTIONS: [&str; 7] = ["General", "Compression", "Marks and Bleeds", "Output", "Advanced", "Security", "Summary"];
|
||||
|
||||
/// The default settings as JSON: what a field an agent left out reads as.
|
||||
static DEFAULTS: LazyLock<Value> = LazyLock::new(|| serde_json::to_value(PdfSettings::default()).unwrap_or_default());
|
||||
|
|
@ -82,7 +101,14 @@ fn params(d: &Dialog) -> Value {
|
|||
}
|
||||
|
||||
/// Save PDF: write the file; the dialog stays open when that fails (bad range, cancelled save…).
|
||||
/// While Save Preset… asks for a name, OK (and Enter) saves the preset instead.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
if d.fields.contains_key(SAVE_AS) {
|
||||
let mut d = d.clone();
|
||||
let r = save_preset(app, &mut d);
|
||||
app.ui.dialog = Some(d);
|
||||
return r;
|
||||
}
|
||||
let r = io::export_pdf(app, params(d));
|
||||
if r.is_ok() {
|
||||
app.ui.dialog = None;
|
||||
|
|
@ -90,6 +116,67 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
|||
r
|
||||
}
|
||||
|
||||
/// The settings as `pdf.preset.save` params (without the artboards and path).
|
||||
fn preset_params(d: &Dialog) -> Value {
|
||||
let mut p = form::params(d);
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
for k in ["range", "path"] {
|
||||
o.remove(k);
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// Save Preset…: save the settings under the typed name and pick that preset.
|
||||
fn save_preset(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<Value, String> {
|
||||
let mut p = preset_params(d);
|
||||
p["name"] = json!(d.str(SAVE_AS));
|
||||
let r = app.run("pdf.preset.save", p)?;
|
||||
d.fields.remove(SAVE_AS);
|
||||
d.fields.insert("preset".into(), r["name"].clone());
|
||||
d.fields.insert("__presets".into(), json!(pdf::presets(&app.session)));
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Open the preset editor on the saved preset `name`, or on a new preset starting from `preset`
|
||||
/// (default: the app default).
|
||||
pub fn open_preset(app: &mut VectorcraftApp, params: &Value) -> Result<Value, String> {
|
||||
let s = |k: &str| params.get(k).and_then(Value::as_str);
|
||||
let (base, name, description, editing) = match s("name") {
|
||||
Some(name) => {
|
||||
let p = app.session.prefs.pdf_presets.iter().find(|p| p.name.eq_ignore_ascii_case(name.trim()));
|
||||
let p = p.ok_or_else(|| format!("no saved PDF preset named `{name}` (built-in presets are read-only: start a new one from them)"))?;
|
||||
(p.name.clone(), p.name.clone(), p.description.clone(), p.name.clone())
|
||||
}
|
||||
None => (s("preset").unwrap_or(pdf::DEFAULT_PRESET).to_string(), app.session.new_pdf_preset_name(), String::new(), String::new()),
|
||||
};
|
||||
let mut fields = settings_fields(app, &json!({ "preset": base }))?;
|
||||
fields.insert("name".into(), json!(name));
|
||||
fields.insert("description".into(), json!(description));
|
||||
fields.insert(EDITING.into(), json!(editing));
|
||||
fields.insert("__section".into(), json!(SECTIONS[0]));
|
||||
app.ui.dialog = Some(Dialog { kind: PRESET_KIND.into(), fields });
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Save Preset (the preset editor's OK): `pdf.preset.save`, then back to Edit → PDF Presets on it.
|
||||
/// A new preset may not take the name of another one.
|
||||
fn confirm_preset(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut p = preset_params(d);
|
||||
let (name, editing) = (d.str("name"), d.str(EDITING));
|
||||
if editing.is_empty() {
|
||||
if app.session.pdf_presets().iter().any(|q| q.name.eq_ignore_ascii_case(name.trim())) {
|
||||
return Err(format!("a preset named `{}` exists", name.trim()));
|
||||
}
|
||||
} else {
|
||||
p["name"] = json!(editing);
|
||||
p["newName"] = json!(name);
|
||||
}
|
||||
let r = app.run("pdf.preset.save", p)?;
|
||||
super::pdf_presets::open(app, r["name"].as_str());
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
// ---------- fields by path (`compression.color.ppi`) ----------
|
||||
|
||||
fn lookup<'a>(m: &'a Map<String, Value>, path: &str) -> Option<&'a Value> {
|
||||
|
|
@ -204,15 +291,14 @@ fn text(ui: &mut egui::Ui, d: &mut Dialog, path: &str, enabled: bool) {
|
|||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
row_with(ui, "Preset:", TOP_LABEL_WIDTH, |ui| {
|
||||
let presets: Vec<&str> =
|
||||
d.fields.get("__presets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
||||
let current = d.str("preset");
|
||||
let chosen = widgets::dropdown(ui, "pdf-preset", ¤t, &presets, 300.0).and_then(|i| presets.get(i)).map(|p| p.to_string());
|
||||
if let Some(name) = chosen {
|
||||
apply_preset(app, d, &name);
|
||||
}
|
||||
});
|
||||
let editor = d.kind == PRESET_KIND;
|
||||
if editor {
|
||||
row_with(ui, "Preset name:", TOP_LABEL_WIDTH, |ui| {
|
||||
form::text_edit(ui, d, "name", 288.0);
|
||||
});
|
||||
} else {
|
||||
preset_rows(app, ui, d);
|
||||
}
|
||||
row_with(ui, "Standard:", TOP_LABEL_WIDTH, |ui| {
|
||||
let picked = pick::<Standard>(ui, d, "standard", 170.0, Standard::supported);
|
||||
let standard = choice::<Standard>(d, "standard").unwrap_or_default();
|
||||
|
|
@ -257,13 +343,48 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
"Advanced" => advanced(ui, d),
|
||||
"Security" => security(ui, d),
|
||||
"Summary" => summary(app, ui, d),
|
||||
_ => general(ui, d),
|
||||
_ => general(ui, d, editor),
|
||||
});
|
||||
});
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
/// The Preset row (the presets, built-in and saved, and Save Preset…) and, while Save Preset…
|
||||
/// asks for a name, the row taking it.
|
||||
fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
row_with(ui, "Preset:", TOP_LABEL_WIDTH, |ui| {
|
||||
let presets = pdf::presets(&app.session);
|
||||
let names: Vec<&str> = presets.iter().map(String::as_str).collect();
|
||||
let chosen = widgets::dropdown(ui, "pdf-preset", &d.str("preset"), &names, 300.0).and_then(|i| names.get(i)).map(|p| p.to_string());
|
||||
if let Some(name) = chosen {
|
||||
apply_preset(app, d, &name);
|
||||
}
|
||||
let asking = d.fields.contains_key(SAVE_AS);
|
||||
if ui
|
||||
.add_enabled_ui(!asking, |ui| widgets::flat_button(ui, "Save Preset…", 96.0))
|
||||
.inner
|
||||
.on_hover_text("Save these settings as a preset")
|
||||
.clicked()
|
||||
{
|
||||
d.fields.insert(SAVE_AS.into(), json!(app.session.new_pdf_preset_name()));
|
||||
}
|
||||
});
|
||||
if d.fields.contains_key(SAVE_AS) {
|
||||
row_with(ui, "Save as preset:", TOP_LABEL_WIDTH, |ui| {
|
||||
form::text_edit(ui, d, SAVE_AS, 200.0);
|
||||
if widgets::flat_button(ui, "Save", 52.0).clicked()
|
||||
&& let Err(e) = save_preset(app, d)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if widgets::flat_button(ui, "Cancel", 60.0).clicked() {
|
||||
d.fields.remove(SAVE_AS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the settings with preset `name`'s (the artboard choice and path stay).
|
||||
fn apply_preset(app: &mut VectorcraftApp, d: &mut Dialog, name: &str) {
|
||||
match settings_fields(app, &json!({ "preset": name })) {
|
||||
|
|
@ -275,7 +396,8 @@ fn apply_preset(app: &mut VectorcraftApp, d: &mut Dialog, name: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
fn general(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// General: the options, then the artboards (Save PDF) or the description (preset editor).
|
||||
fn general(ui: &mut egui::Ui, d: &mut Dialog, editor: bool) {
|
||||
heading(ui, "Options");
|
||||
let plain = choice::<Standard>(d, "standard").unwrap_or_default() == Standard::None;
|
||||
flag(ui, d, "preserveEditing", "Preserve editing capabilities", plain);
|
||||
|
|
@ -283,6 +405,13 @@ fn general(ui: &mut egui::Ui, d: &mut Dialog) {
|
|||
flag(ui, d, "fastWebView", "Optimize for fast web view", true);
|
||||
flag(ui, d, "viewAfterSaving", "View PDF after saving", true);
|
||||
flag(ui, d, "createLayers", "Create PDF layers from top-level layers", true);
|
||||
if editor {
|
||||
heading(ui, "Description");
|
||||
if let Some(text) = widgets::text_field(ui, "pdf-preset-description", Some(&d.str("description")), 460.0, 3) {
|
||||
d.fields.insert("description".into(), json!(text));
|
||||
}
|
||||
return;
|
||||
}
|
||||
heading(ui, "Artboards");
|
||||
let mut all = d.bool("__allArtboards");
|
||||
ui.horizontal(|ui| {
|
||||
|
|
@ -455,7 +584,7 @@ fn security(ui: &mut egui::Ui, d: &mut Dialog) {
|
|||
}
|
||||
|
||||
/// The section a changed option belongs to (for the Summary's order).
|
||||
fn section_of(option: &str) -> usize {
|
||||
pub(super) fn section_of(option: &str) -> usize {
|
||||
match option.split('.').next().unwrap_or_default() {
|
||||
"compression" => 1,
|
||||
"marks" | "bleed" => 2,
|
||||
|
|
@ -468,7 +597,7 @@ fn section_of(option: &str) -> usize {
|
|||
|
||||
/// `compression.color.abovePpi` → "Compression › Color › Above Ppi": the section, then the keys
|
||||
/// (without the section's own object), `thumbnails` → "General › Thumbnails".
|
||||
fn option_label(option: &str) -> String {
|
||||
pub(super) fn option_label(option: &str) -> String {
|
||||
let section = SECTIONS.get(section_of(option)).copied().unwrap_or(SECTIONS[0]);
|
||||
let mut keys = option.split('.').peekable();
|
||||
keys.next_if(|k| k.eq_ignore_ascii_case(section));
|
||||
|
|
@ -507,7 +636,8 @@ fn summary(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
|||
let mut changed: Vec<&Value> = v["changed"].as_array().map(|a| a.iter().collect()).unwrap_or_default();
|
||||
changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default()));
|
||||
if changed.is_empty() {
|
||||
note(ui, "Every option is at its default.");
|
||||
let base = if d.kind == PRESET_KIND { pdf::DEFAULT_PRESET } else { "the preset" };
|
||||
note(ui, &format!("Every option matches {base}."));
|
||||
}
|
||||
for c in changed {
|
||||
let value = match &c["value"] {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ use crate::VectorcraftApp;
|
|||
use crate::dialogs::svg_options;
|
||||
|
||||
/// Open bytes of any readable format as a new document (templates open untitled); swatch and
|
||||
/// graphic style library files open in the library panel and flattener presets files are imported.
|
||||
/// graphic style library files open in the library panel and flattener and PDF presets files are
|
||||
/// imported.
|
||||
pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<String>) -> Result<(), String> {
|
||||
let ext = fileio::extension(name);
|
||||
if vectorcraft_engine::cmd::flatten::PRESET_EXTS.contains(&ext.as_str()) {
|
||||
let r = app.run("flattener.presets.import", serde_json::json!({"data": String::from_utf8_lossy(bytes)}))?;
|
||||
let presets = [
|
||||
(vectorcraft_engine::cmd::flatten::PRESET_EXTS, "flattener.presets.import", "flattener presets"),
|
||||
(vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS, "pdf.preset.import", "PDF presets"),
|
||||
];
|
||||
if let Some((_, import, what)) = presets.iter().find(|(exts, ..)| exts.contains(&ext.as_str())) {
|
||||
let r = app.run(import, serde_json::json!({"data": String::from_utf8_lossy(bytes)}))?;
|
||||
let names: Vec<&str> = r["imported"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
app.status(format!("Imported flattener presets: {}", names.join(", ")));
|
||||
app.status(format!("Imported {what}: {}", names.join(", ")));
|
||||
return Ok(());
|
||||
}
|
||||
let swatches = vectorcraft_engine::cmd::swatchlib::LIBRARY_EXTS.contains(&ext.as_str());
|
||||
|
|
|
|||
|
|
@ -404,6 +404,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"",
|
||||
"{} open Document Raster Effects Settings (dialog `rasterEffectsSettings`: the document.rasterEffectsSettings fields); OK runs it. The menu item opens it too",
|
||||
),
|
||||
(
|
||||
"ui.pdfPresetsDialog",
|
||||
"PDF Presets…",
|
||||
"",
|
||||
"{selected?: preset name} open Edit → PDF Presets (dialog `pdfPresets`, field `selected`): the built-in presets (read-only) and the saved ones, with the selected one's description and settings. New… and Edit… open the preset editor (ui.pdfPresetDialog); Delete, Import… and Export… run pdf.preset.delete / import / export",
|
||||
),
|
||||
(
|
||||
"ui.pdfPresetDialog",
|
||||
"PDF Preset",
|
||||
"",
|
||||
"{name?: a saved preset to edit | preset?: the preset a new one starts from (default VectorCraft Default)} open the preset editor (dialog `pdfPreset`: the Save PDF dialog's option fields plus `name` and `description`); OK runs pdf.preset.save and returns to PDF Presets",
|
||||
),
|
||||
];
|
||||
|
||||
/// Handle a UI command. `None` = not a UI command (the engine handles it).
|
||||
|
|
@ -777,6 +789,11 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
"file.exportAs" => io::export(app, s("format").as_deref(), s("path"), p),
|
||||
"ui.fileInfoDialog" => crate::dialogs::file_info::open(app),
|
||||
"ui.rasterEffectsSettingsDialog" => crate::dialogs::raster_effects::open(app),
|
||||
"ui.pdfPresetsDialog" => {
|
||||
crate::dialogs::pdf_presets::open(app, s("selected").as_deref());
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"ui.pdfPresetDialog" => crate::dialogs::open_pdf_preset(app, p),
|
||||
_ => return None,
|
||||
};
|
||||
Some(r)
|
||||
|
|
@ -1090,7 +1107,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
|||
Sep,
|
||||
c("Transparency Flattener Presets…", "ui.flattenerPresetsDialog"),
|
||||
todo("Print Presets…"),
|
||||
todo("PDF Presets…"),
|
||||
c("PDF Presets…", "ui.pdfPresetsDialog"),
|
||||
cp("Perspective Grid Presets…", "perspective.grid.preset", json!({"kind": 2})),
|
||||
Sep,
|
||||
c("Color Settings…", "edit.colorSettings"),
|
||||
|
|
|
|||
|
|
@ -74,7 +74,12 @@ fn swatches_menu_offers_save_swatch_library() {
|
|||
#[test]
|
||||
fn edit_menu_lists_pdf_presets() {
|
||||
let app = app();
|
||||
assert!(menus::menu_entries(&app).iter().any(|e| e.path == ["Edit"] && e.label == "PDF Presets…" && !e.enabled));
|
||||
// Implemented (M4.44): enabled, and it opens Edit → PDF Presets.
|
||||
assert!(
|
||||
menus::menu_entries(&app)
|
||||
.iter()
|
||||
.any(|e| e.path == ["Edit"] && e.label == "PDF Presets…" && e.enabled && e.command.as_deref() == Some("ui.pdfPresetsDialog"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -165,6 +165,15 @@ built-in presets don't change. New, Delete, Import… and Export… are `flatten
|
|||
Opening a `.vcflattener` file with `app.open` imports its presets. In the Flatten Transparency dialog, Save Preset…
|
||||
keeps the dialog's options as a saved preset.
|
||||
|
||||
PDF presets: `ui.pdfPresetsDialog {selected?}` opens Edit → PDF Presets (dialog `pdfPresets`, field `selected`: a
|
||||
preset name). Built-in presets are read-only. New… and Edit… open the preset editor, which `ui.pdfPresetDialog {name?
|
||||
(a saved preset to edit) | preset? (the preset a new one starts from)}` opens too: dialog `pdfPreset`, the Save PDF
|
||||
dialog's option fields plus `name` and `description`; `ui.dialog.confirm` runs `pdf.preset.save` and returns to PDF
|
||||
Presets (a new preset can't take a name in use). Delete, Import… and Export… are `pdf.preset.delete`,
|
||||
`pdf.preset.import` and `pdf.preset.export`; `ui.dialog.confirm` closes. Opening a `.vcpdfpresets` file with `app.open`
|
||||
imports its presets. In the Save PDF dialog, Save Preset… shows a name field (`__savePresetAs`): while it shows,
|
||||
`ui.dialog.confirm` saves the dialog's settings as that preset and selects it instead of writing the PDF.
|
||||
|
||||
Flattener Preview: `window.panel {panel: "flattenerPreview"}` shows the panel; `ui.flattenerPreview {highlight?,
|
||||
overprints?, preset?, options?, showOptions?}` sets it (a preset loads its options, `options` adjust them), shows it
|
||||
and refreshes its snapshot of the document, answering as `flattener.preview` does. `ui.inspect` reports the settings
|
||||
|
|
|
|||
19
docs/mcp.md
19
docs/mcp.md
|
|
@ -183,7 +183,7 @@ PDF files take the Save PDF dialog's options: `preset`, `standard`, `compatibili
|
|||
`compression`, `marks`, `bleed`, `output`, `advanced` and `security` sections (`list_commands` with filter `exportPdf`
|
||||
documents every field). `document.exportPdf` and `export` (format `pdf`, the options in `options`) return `warnings`:
|
||||
options accepted but not applied yet, and features approximated or left out. PDF/X, passwords and PDF/A-2b at 2.0 are refused.
|
||||
`document.pdfSettings` lists the options that differ from the defaults and the warnings without writing a file.
|
||||
`document.pdfSettings` lists the options that differ from the preset and the warnings without writing a file.
|
||||
|
||||
Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based),
|
||||
`cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets)
|
||||
|
|
@ -959,3 +959,20 @@ bounds, or the artboard less a 36 pt margin.
|
|||
```json
|
||||
{"name":"run_command","arguments":{"command":"file.place","params":{"path":"/tmp/notes.txt","text":{"removeLineReturns":true,"removeParagraphReturns":true}}}}
|
||||
```
|
||||
|
||||
## PDF presets
|
||||
|
||||
`pdf.preset.list` lists the built-in presets (`VectorCraft Default`, which preserves editing, then High Quality Print,
|
||||
Press Quality, Smallest File Size and the PDF/X presets, `builtIn: true`; `supported: false` where the writer can't
|
||||
produce the standard yet) and the saved ones, each with its `description` and `settings`. `pdf.preset.save {name?,
|
||||
newName?, description?, preset?, …document.exportPdf options}` creates or changes a saved preset (a change starts from
|
||||
the preset's own settings; `newName` renames it; built-in presets are read-only; passwords are never stored);
|
||||
`pdf.preset.delete {name}` deletes one. `pdf.preset.export {names?, path?}` writes them as a `.vcpdfpresets` JSON file
|
||||
(without `path` it returns `data`), and `pdf.preset.import {path? | data? | dataBase64?, replace?}` adds a file's
|
||||
presets (names in use get a number unless `replace`). Saved presets live with the preferences and work as `preset`
|
||||
in `document.exportPdf`, `document.pdfSettings`, `document.export`/`serialize` with format `pdf` and `.ai` saves.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"pdf.preset.save","params":{"name":"Web","preset":"Smallest File Size","compatibility":"1.5"}}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"preset":"Web","path":"/tmp/web.pdf"}}}
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue