mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
M4.32: PDF warnings, Summary section, View PDF after Saving and status text
document.export and document.serialize return the encoder's warnings (PDF: options not applied yet, features approximated or left out, overprint). document.pdfSettings lists the options that differ from the defaults and the warnings (includeDocument adds the active document's own from an in-memory export); the dialog's Summary section shows both. View PDF after Saving opens the written file as a file:// URL through Services.open_url; the status line reports the notes. The PDF writer's raster-effects warning now names what it leaves out (objects of a kind, fills, strokes) instead of claiming raster effects are never exported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4304a7c0b1
commit
a40bce615a
@@ -101,10 +101,19 @@ fn check_format_size(f: &Format, w: f64, h: f64) -> Result<()> {
|
||||
/// options from `p` (see `document.formats`). Raster formats leave template layers out, and no
|
||||
/// format writes the opacity-mask editing layer.
|
||||
pub fn encode(doc: &Document, format: &str, p: &Value) -> Result<Vec<u8>> {
|
||||
encode_with_warnings(doc, format, p).map(|(bytes, _)| bytes)
|
||||
}
|
||||
|
||||
/// Like [`encode`], also returning the encoder's warnings (PDF: options not applied yet, features
|
||||
/// approximated or left out; the other formats have none).
|
||||
pub fn encode_with_warnings(doc: &Document, format: &str, p: &Value) -> Result<(Vec<u8>, Vec<String>)> {
|
||||
let f = super::writable(C, Some(format), None)?;
|
||||
let doc = &*doc.without_edit_modes();
|
||||
let n = doc.artboards.len();
|
||||
Ok(match f.id {
|
||||
if f.id == "pdf" {
|
||||
return super::pdf::encode(C, doc, p);
|
||||
}
|
||||
let bytes = match f.id {
|
||||
"vectorcraft" => vectorcraft_format::save_file(doc),
|
||||
"svg" => {
|
||||
let o: SvgOptions = options(f, p)?;
|
||||
@@ -112,7 +121,6 @@ pub fn encode(doc: &Document, format: &str, p: &Value) -> Result<Vec<u8>> {
|
||||
let opts = vectorcraft_svg::ExportOptions { artboard, outline_text: o.outline_text.unwrap_or(false), ..Default::default() };
|
||||
vectorcraft_svg::export(doc, &opts).into_bytes()
|
||||
}
|
||||
"pdf" => super::pdf::encode(C, doc, p)?.0,
|
||||
"png" | "jpg" | "webp" => {
|
||||
let o: RasterOptions = options(f, p)?;
|
||||
let region = doc.artboards[boards(o.boards.one(n))?].rect;
|
||||
@@ -128,5 +136,6 @@ pub fn encode(doc: &Document, format: &str, p: &Value) -> Result<Vec<u8>> {
|
||||
.map_err(EngineError::Other)?
|
||||
}
|
||||
_ => return Err(bad(C, format!("no encoder for {} yet", f.label))),
|
||||
})
|
||||
};
|
||||
Ok((bytes, vec![]))
|
||||
}
|
||||
|
||||
@@ -8,22 +8,22 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
|
||||
use super::super::*;
|
||||
use super::{ARTBOARD_PARAMS, ArtboardPick, Format, create_dir, encode, writable, write_file, write_or_return};
|
||||
use super::{ARTBOARD_PARAMS, ArtboardPick, Format, create_dir, encode, encode_with_warnings, writable, write_file, write_or_return};
|
||||
|
||||
pub(super) fn serialize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let f = writable("document.serialize", Some(str_param(p, "format").unwrap_or("vectorcraft")), None)?;
|
||||
let bytes = encode(&s.doc()?.doc, f.id, p)?;
|
||||
let (bytes, warnings) = encode_with_warnings(&s.doc()?.doc, f.id, p)?;
|
||||
if f.id == "svg" {
|
||||
return Ok(json!({ "text": String::from_utf8_lossy(&bytes) }));
|
||||
return Ok(json!({ "text": String::from_utf8_lossy(&bytes), "warnings": warnings }));
|
||||
}
|
||||
Ok(json!({ "dataBase64": vectorcraft_format::base64_encode(&bytes) }))
|
||||
Ok(json!({ "dataBase64": vectorcraft_format::base64_encode(&bytes), "warnings": warnings }))
|
||||
}
|
||||
|
||||
pub(super) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_param(p, "path");
|
||||
let f = writable("document.export", str_param(p, "format"), path)?;
|
||||
let bytes = encode(&s.doc()?.doc, f.id, p)?;
|
||||
write_or_return(path, &bytes, json!({ "format": f.id }))
|
||||
let (bytes, warnings) = encode_with_warnings(&s.doc()?.doc, f.id, p)?;
|
||||
write_or_return(path, &bytes, json!({ "format": f.id, "warnings": warnings }))
|
||||
}
|
||||
|
||||
/// `p` without its artboard choice (for documents made of one synthetic artboard).
|
||||
|
||||
@@ -19,7 +19,7 @@ mod save;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub use encode::{ARTBOARD_PARAMS, ArtboardPick, encode};
|
||||
pub use encode::{ARTBOARD_PARAMS, ArtboardPick, encode, encode_with_warnings};
|
||||
pub use load::{Loaded, RasterImage, detect, load, open_bytes, raster_image};
|
||||
|
||||
use super::*;
|
||||
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Serialize Document",
|
||||
[],
|
||||
None,
|
||||
"{format?: vectorcraft (default)|svg|pdf|png|jpg|webp, …the format's options (see document.formats)} → {text} for svg, else {dataBase64}",
|
||||
"{format?: vectorcraft (default)|svg|pdf|png|jpg|webp, …the format's options (see document.formats)} → {text, warnings} for svg, else {dataBase64, warnings}",
|
||||
has_doc,
|
||||
export::serialize
|
||||
),
|
||||
@@ -59,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: svg|pdf|png|jpg|webp|vectorcraft (default: from the path's extension, else png), artboard?: 0, artboards?: [i…], range?: \"1-3, 5\" (1-based; PDF writes one page per artboard, default all; the other formats write one artboard), scale?: 1 (raster), quality?: 90 (jpg), outlineText?: bool (svg), …the PDF options of document.exportPdf} → {path, format, bytes}; no path → {dataBase64, format, bytes}. Never changes the document's path",
|
||||
"{path?, format?: svg|pdf|png|jpg|webp|vectorcraft (default: from the path's extension, else png), artboard?: 0, artboards?: [i…], range?: \"1-3, 5\" (1-based; PDF writes one page per artboard, default all; the other formats write one artboard), scale?: 1 (raster), quality?: 90 (jpg), outlineText?: bool (svg), …the PDF options of document.exportPdf} → {path, format, bytes, warnings}; no path → {dataBase64, format, bytes, warnings}. Never changes the document's path",
|
||||
has_doc,
|
||||
export::export
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! PDF export settings for every PDF path (`document.export {format: pdf}`, Export for Screens,
|
||||
//! `document.exportPdf` and the Save PDF dialog): presets, the options parsed over them and the
|
||||
//! warnings that come back.
|
||||
//! `document.exportPdf` and the Save PDF dialog): presets, the options parsed over them, the
|
||||
//! warnings that come back, and `document.pdfSettings` (the dialog's Summary).
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -33,15 +33,26 @@ pub(super) const OPTIONS: &[FormatOption] = &[
|
||||
];
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"document.exportPdf",
|
||||
"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?, thumbnails?, fastWebView?, viewAfterSaving?, 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
|
||||
)]
|
||||
vec![
|
||||
cmd!(
|
||||
"document.exportPdf",
|
||||
"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?, 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
|
||||
),
|
||||
cmd!(
|
||||
query "document.pdfSettings",
|
||||
"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…)",
|
||||
always,
|
||||
pdf_settings
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Every preset name (built-in first).
|
||||
@@ -117,3 +128,31 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (bytes, warnings) = encode(C, &s.doc()?.doc, p)?;
|
||||
write_or_return(str_param(p, "path"), &bytes, json!({ "warnings": warnings }))
|
||||
}
|
||||
|
||||
/// `(path, value)` for every leaf of `v` that differs from `default` (`compression.color.ppi`).
|
||||
fn changed(prefix: &str, v: &Value, default: &Value, out: &mut Vec<Value>) {
|
||||
match (v, default) {
|
||||
(Value::Object(o), Value::Object(d)) => {
|
||||
for (k, x) in o {
|
||||
let path = if prefix.is_empty() { k.clone() } else { format!("{prefix}.{k}") };
|
||||
changed(&path, x, d.get(k).unwrap_or(&Value::Null), out);
|
||||
}
|
||||
}
|
||||
_ if v != default => out.push(json!({ "option": prefix, "value": v })),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn pdf_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const Q: &str = "document.pdfSettings";
|
||||
let set = settings(Q, p)?;
|
||||
let v = serde_json::to_value(&set).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let default = serde_json::to_value(PdfSettings::default()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let mut diff = vec![];
|
||||
changed("", &v, &default, &mut diff);
|
||||
let warnings = match s.active() {
|
||||
Some(st) if bool_or(p, "includeDocument", false) => encode(Q, &st.doc, p)?.1,
|
||||
_ => set.warnings(),
|
||||
};
|
||||
Ok(json!({ "settings": v, "presets": presets(), "changed": diff, "warnings": warnings }))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `document.exportPdf` and the PDF options of every PDF export.
|
||||
//! `document.exportPdf`, `document.pdfSettings` and the PDF options of every PDF export.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -51,6 +51,7 @@ fn export_pdf_writes_the_compatibility_header_and_the_range() {
|
||||
// The same options through document.export and document.serialize.
|
||||
let v = s.execute("document.export", &as_export(&json!({"compatibility": "1.4", "range": "2"}))).unwrap();
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.4") && pages(&b64(&v)) == 1);
|
||||
assert!(v["warnings"].is_array());
|
||||
let v = s.execute("document.serialize", &as_export(&json!({"compatibility": "1.6"}))).unwrap();
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.6"));
|
||||
}
|
||||
@@ -86,15 +87,15 @@ fn options_apply_over_the_preset() {
|
||||
assert!(String::from_utf8_lossy(&a).contains(" re") || String::from_utf8_lossy(&a).contains(" l"));
|
||||
// `null` (the default document.formats lists) keeps the preset's value, at any depth.
|
||||
let p = json!({"compatibility": null, "compression": null, "security": {"openPassword": null}, "marks": {"weight": null}});
|
||||
let v = export(&mut s, p);
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.7"));
|
||||
assert!(warnings(&v).is_empty(), "{v}");
|
||||
let v = s.execute("document.pdfSettings", &p).unwrap();
|
||||
assert_eq!(v["changed"], json!([]), "{v}");
|
||||
assert!(b64(&export(&mut s, p)).starts_with(b"%PDF-1.7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_a_refuses_pdf_2() {
|
||||
let mut s = session(1);
|
||||
let e = s.execute("document.exportPdf", &json!({"standard": "pdfA2b", "compatibility": "2.0"})).unwrap_err();
|
||||
let e = s.execute("document.pdfSettings", &json!({"standard": "pdfA2b", "compatibility": "2.0"})).unwrap_err();
|
||||
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{e}");
|
||||
let pdf = b64(&export(&mut s, json!({"standard": "pdfA2b", "compatibility": "1.6"})));
|
||||
assert!(pdf.starts_with(b"%PDF-1.6") && String::from_utf8_lossy(&pdf).contains("pdfaid"));
|
||||
@@ -106,7 +107,7 @@ fn options_not_applied_yet_and_document_features_warn() {
|
||||
let w = warnings(&export(&mut s, json!({"thumbnails": true, "marks": {"trim": true}})));
|
||||
assert_eq!(w.len(), 2, "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("thumbnails")) && w.iter().any(|w| w.contains("marks")));
|
||||
// A pattern stroke is approximated: its warning comes back.
|
||||
// A pattern stroke is approximated: its warning comes back from every PDF path.
|
||||
let tile = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [tile]})).unwrap();
|
||||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
@@ -115,6 +116,9 @@ fn options_not_applied_yet_and_document_features_warn() {
|
||||
s.execute("paint.setStroke", &json!({"ids": [frame], "swatch": "Dots"})).unwrap();
|
||||
let pattern = |w: Vec<String>| w.iter().any(|w| w.contains("pattern strokes"));
|
||||
assert!(pattern(warnings(&export(&mut s, json!({})))));
|
||||
assert!(pattern(warnings(&s.execute("document.export", &as_export(&json!({}))).unwrap())));
|
||||
// Other formats have none.
|
||||
assert!(warnings(&s.execute("document.export", &json!({"format": "png"})).unwrap()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -133,6 +137,33 @@ fn export_pdf_writes_a_path() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_settings_summarise_changes_and_warnings() {
|
||||
let mut s = Session::new();
|
||||
let v = s.execute("document.pdfSettings", &json!({})).unwrap();
|
||||
assert_eq!(v["settings"]["compatibility"], "1.7");
|
||||
assert_eq!(v["presets"][0], DEFAULT_PRESET);
|
||||
assert_eq!(v["changed"], json!([]));
|
||||
assert_eq!(v["warnings"], json!([]));
|
||||
let p = json!({"compatibility": "1.5", "compression": {"compressText": false}, "createLayers": true, "security": {"copy": false, "openPassword": ""}});
|
||||
let v = s.execute("document.pdfSettings", &p).unwrap();
|
||||
let changed: Vec<&str> = v["changed"].as_array().unwrap().iter().map(|c| c["option"].as_str().unwrap()).collect();
|
||||
assert_eq!(changed, ["compatibility", "compression.compressText", "createLayers", "security.copy"], "key order");
|
||||
assert_eq!(v["changed"][0]["value"], "1.5");
|
||||
assert_eq!(v["warnings"].as_array().unwrap().len(), 2, "layers and permissions: {}", v["warnings"]);
|
||||
assert!(!v.to_string().contains("Password"), "passwords never come back");
|
||||
assert!(s.execute("document.pdfSettings", &json!({"compatibility": "9"})).is_err());
|
||||
// With the document: the in-memory export adds the document's own warnings.
|
||||
let mut s = session(1);
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
let g = s.doc().unwrap().selection.objects[0].0;
|
||||
s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [g]})).unwrap();
|
||||
assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["warnings"], json!([]));
|
||||
let full = s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap();
|
||||
assert!(warnings(&full).iter().any(|w| w.contains("group objects")), "{}", full["warnings"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_list_the_pdf_options() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -391,9 +391,11 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
/// Warn when `effects` (an object's, a fill's or a stroke's) has a visible raster effect.
|
||||
fn warn_raster(&mut self, effects: &[vectorcraft_doc::Effect]) {
|
||||
/// Warn when `effects` has a raster effect: the app renders them to images before export
|
||||
/// (`vectorcraft_engine::export_pdf`), so the ones still here (on `what`) can't be written.
|
||||
fn warn_raster(&mut self, effects: &[vectorcraft_doc::Effect], what: impl FnOnce() -> String) {
|
||||
if effects.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id)) {
|
||||
self.warn("raster effects (shadows, glows, blur, feather) are not exported to PDF yet");
|
||||
self.warn(format!("raster effects (shadows, glows, blur, feather) on {} are left out of the PDF", what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +588,7 @@ impl Exporter<'_> {
|
||||
None if !n.is_container() => return,
|
||||
_ => {}
|
||||
}
|
||||
self.warn_raster(&n.appearance.effects);
|
||||
self.warn_raster(&n.appearance.effects, || format!("{} objects", n.kind_label().to_lowercase()));
|
||||
let container = n.is_container() && !matches!(n.kind, NodeKind::Compound { .. });
|
||||
// Whether this container's children knock each other out (a knockout group is written as a group).
|
||||
let knockout = n.knocks_out(self.knockout);
|
||||
@@ -681,7 +683,7 @@ impl Exporter<'_> {
|
||||
if !fl.visible || fl.paint.is_none() {
|
||||
continue;
|
||||
}
|
||||
self.warn_raster(&fl.effects);
|
||||
self.warn_raster(&fl.effects, || "fills".into());
|
||||
// Pattern fills: the tile instances covering the shape, clipped to it.
|
||||
let doc = self.doc;
|
||||
if let Paint::Pattern { pattern, xf } = &fl.paint
|
||||
@@ -716,7 +718,7 @@ impl Exporter<'_> {
|
||||
if !st.visible || st.paint.is_none() || st.width <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
self.warn_raster(&st.effects);
|
||||
self.warn_raster(&st.effects, || "strokes".into());
|
||||
self.stroke(s, bp, &path, r, st, page, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! File → Save as PDF: the Save PDF dialog. Preset, Standard and Compatibility on top, then the
|
||||
//! sections General, Compression, Marks and Bleeds, Output, Advanced and Security.
|
||||
//! sections General, Compression, Marks and Bleeds, Output, Advanced, Security and Summary.
|
||||
//!
|
||||
//! 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`].
|
||||
//! `ui.dialog.set`. OK runs `document.exportPdf` through [`io::export_pdf`]; the Summary is
|
||||
//! `document.pdfSettings`.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -24,7 +25,7 @@ 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; 6] = ["General", "Compression", "Marks and Bleeds", "Output", "Advanced", "Security"];
|
||||
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());
|
||||
@@ -33,6 +34,9 @@ static DEFAULTS: LazyLock<Value> = LazyLock::new(|| serde_json::to_value(PdfSett
|
||||
const WEIGHTS: [f64; 3] = [0.125, 0.25, 0.5];
|
||||
const WEIGHT_LABELS: [&str; 3] = ["0.125 pt", "0.25 pt", "0.5 pt"];
|
||||
|
||||
/// The Destination entry for the document's own profile (stored as "").
|
||||
const DOCUMENT_PROFILE: &str = "Document profile";
|
||||
|
||||
const LABEL_WIDTH: f32 = 150.0;
|
||||
/// Width of the section list (its frame adds 6 px a side).
|
||||
const LIST_WIDTH: f32 = 150.0;
|
||||
@@ -156,20 +160,17 @@ fn choice_index<T: Choice>(d: &Dialog, path: &str) -> Option<usize> {
|
||||
|
||||
/// The choice at `path`.
|
||||
fn choice<T: Choice>(d: &Dialog, path: &str) -> Option<T> {
|
||||
choice_index::<T>(d, path).map(|i| T::ALL[i])
|
||||
choice_index::<T>(d, path).and_then(|i| T::ALL.get(i).copied())
|
||||
}
|
||||
|
||||
/// A dropdown of the choices `T` (options for which `enabled` is false are greyed).
|
||||
fn pick<T: Choice>(ui: &mut egui::Ui, d: &mut Dialog, path: &str, width: f32, enabled: impl Fn(T) -> bool) -> bool {
|
||||
let current = choice_index::<T>(d, path);
|
||||
let label = current.map_or("", |i| T::LABELS[i]);
|
||||
match widgets::dropdown_with(ui, path, label, T::LABELS, width, |i| enabled(T::ALL[i])) {
|
||||
Some(i) => {
|
||||
set(d, path, json!(T::IDS[i]));
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
let label = current.and_then(|i| T::LABELS.get(i)).copied().unwrap_or_default();
|
||||
let chosen = widgets::dropdown_with(ui, path, label, T::LABELS, width, |i| T::ALL.get(i).is_some_and(|c| enabled(*c)));
|
||||
let Some(id) = chosen.and_then(|i| T::IDS.get(i)) else { return false };
|
||||
set(d, path, json!(id));
|
||||
true
|
||||
}
|
||||
|
||||
fn number(ui: &mut egui::Ui, d: &mut Dialog, path: &str, suffix: &str, enabled: bool) {
|
||||
@@ -206,7 +207,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
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).map(|i| presets[i].to_string());
|
||||
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);
|
||||
}
|
||||
@@ -250,6 +251,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
"Output" => output(ui, d),
|
||||
"Advanced" => advanced(ui, d),
|
||||
"Security" => security(ui, d),
|
||||
"Summary" => summary(app, ui, d),
|
||||
_ => general(ui, d),
|
||||
});
|
||||
});
|
||||
@@ -351,9 +353,10 @@ fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
row(ui, "Trim mark weight:", |ui| {
|
||||
let w = get(d, "marks.weight").as_f64().unwrap_or(0.25);
|
||||
let current = WEIGHTS.iter().position(|x| (x - w).abs() < 1e-9).map_or_else(|| format!("{w} pt"), |i| WEIGHT_LABELS[i].to_string());
|
||||
if let Some(i) = widgets::dropdown(ui, "marks.weight", ¤t, &WEIGHT_LABELS, 130.0) {
|
||||
set(d, "marks.weight", json!(WEIGHTS[i]));
|
||||
let current =
|
||||
WEIGHTS.iter().zip(WEIGHT_LABELS).find(|(x, _)| (*x - w).abs() < 1e-9).map_or_else(|| format!("{w} pt"), |(_, l)| l.to_string());
|
||||
if let Some(x) = widgets::dropdown(ui, "marks.weight", ¤t, &WEIGHT_LABELS, 130.0).and_then(|i| WEIGHTS.get(i)) {
|
||||
set(d, "marks.weight", json!(x));
|
||||
}
|
||||
});
|
||||
row(ui, "Offset:", |ui| length(ui, d, "marks.offset", unit, true));
|
||||
@@ -379,11 +382,11 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let converting = get(d, "output.conversion") != ColorConversion::None.id();
|
||||
row(ui, "Destination:", |ui| {
|
||||
let profiles = vectorcraft_color::cms::profiles();
|
||||
let names: Vec<&str> = std::iter::once("Document profile").chain(profiles.iter().map(|p| p.name.as_str())).collect();
|
||||
let current = get(d, "output.destination").as_str().filter(|s| !s.is_empty()).unwrap_or(names[0]).to_string();
|
||||
let names: Vec<&str> = std::iter::once(DOCUMENT_PROFILE).chain(profiles.iter().map(|p| p.name.as_str())).collect();
|
||||
let current = get(d, "output.destination").as_str().filter(|s| !s.is_empty()).unwrap_or(DOCUMENT_PROFILE).to_string();
|
||||
ui.add_enabled_ui(converting, |ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, "output.destination", ¤t, &names, 300.0) {
|
||||
set(d, "output.destination", json!(if i == 0 { "" } else { names[i] }));
|
||||
set(d, "output.destination", json!(if i == 0 { "" } else { names.get(i).copied().unwrap_or_default() }));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -445,6 +448,79 @@ fn security(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
flag(ui, d, "security.plaintextMetadata", "Enable plaintext metadata", false);
|
||||
}
|
||||
|
||||
/// The section a changed option belongs to (for the Summary's order).
|
||||
fn section_of(option: &str) -> usize {
|
||||
match option.split('.').next().unwrap_or_default() {
|
||||
"compression" => 1,
|
||||
"marks" | "bleed" => 2,
|
||||
"output" => 3,
|
||||
"advanced" => 4,
|
||||
"security" => 5,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// `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 {
|
||||
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));
|
||||
std::iter::once(section.to_string()).chain(keys.map(|k| form::humanize(k).trim_end_matches(':').to_string())).collect::<Vec<_>>().join(" › ")
|
||||
}
|
||||
|
||||
/// `document.pdfSettings` for the dialog with the document's warnings (an export in memory),
|
||||
/// cached until a field or the document changes.
|
||||
fn summary_of(app: &mut VectorcraftApp, ctx: &egui::Context, p: &Value) -> Result<Value, String> {
|
||||
let revision = app.session.active().map_or(0, |s| s.revision);
|
||||
let stamp = egui::Id::new((p.to_string(), revision));
|
||||
let key = egui::Id::new("save-pdf-summary");
|
||||
if let Some((s, v)) = ctx.data(|m| m.get_temp::<(egui::Id, Result<Value, String>)>(key))
|
||||
&& s == stamp
|
||||
{
|
||||
return v;
|
||||
}
|
||||
let mut q = p.clone();
|
||||
q["includeDocument"] = json!(true);
|
||||
let v = app.session.execute("document.pdfSettings", &q).map_err(|e| e.to_string());
|
||||
ctx.data_mut(|m| m.insert_temp(key, (stamp, v.clone())));
|
||||
v
|
||||
}
|
||||
|
||||
fn summary(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let v = match summary_of(app, ui.ctx(), ¶ms(d)) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
heading(ui, "Error");
|
||||
ui.label(egui::RichText::new(format!("⚠ {e}")).color(t.text));
|
||||
return;
|
||||
}
|
||||
};
|
||||
heading(ui, "Options");
|
||||
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.");
|
||||
}
|
||||
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));
|
||||
}
|
||||
heading(ui, "Warnings");
|
||||
let warnings = v["warnings"].as_array().map(Vec::as_slice).unwrap_or_default();
|
||||
if warnings.is_empty() {
|
||||
note(ui, "None.");
|
||||
}
|
||||
for w in warnings {
|
||||
ui.label(egui::RichText::new(format!("⚠ {}", w.as_str().unwrap_or_default())).color(t.text));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
@@ -457,21 +533,22 @@ mod tests {
|
||||
|
||||
type Log = Rc<RefCell<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
/// An app with three artboards whose writer records what it gets.
|
||||
fn app() -> (VectorcraftApp, Log) {
|
||||
let written = Log::default();
|
||||
let w = written.clone();
|
||||
/// An app with two artboards whose writer and URL opener record what they get.
|
||||
fn app() -> (VectorcraftApp, Log, Log) {
|
||||
let (written, opened) = (Log::default(), Log::default());
|
||||
let (w, o) = (written.clone(), opened.clone());
|
||||
let services = Services {
|
||||
write: Some(Box::new(move |p: &str, b: &[u8]| {
|
||||
w.borrow_mut().push((p.to_string(), b.to_vec()));
|
||||
Ok(())
|
||||
})),
|
||||
open_url: Some(Box::new(move |u: &str| o.borrow_mut().push((u.to_string(), vec![])))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("file.new", json!({"width": 120, "height": 90, "artboards": 3})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
(app, written)
|
||||
(app, written, opened)
|
||||
}
|
||||
|
||||
/// One headless frame of the dialog layer.
|
||||
@@ -488,7 +565,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_section_draws() {
|
||||
let (mut app, _) = app();
|
||||
let (mut app, _, _) = app();
|
||||
app.run("file.export.pdf", json!({})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND));
|
||||
assert_eq!(super::super::DialogKind::of(KIND), Some(super::super::DialogKind::SavePdf));
|
||||
@@ -497,15 +574,22 @@ mod tests {
|
||||
frame(&mut app);
|
||||
assert!(app.ui.dialog.is_some(), "{s} closed the dialog");
|
||||
}
|
||||
// The Summary lists what changed and the warnings for options not applied yet.
|
||||
set_field(&mut app, "thumbnails", json!(true));
|
||||
let p = params(app.ui.dialog.as_ref().unwrap());
|
||||
let v = summary_of(&mut app, &egui::Context::default(), &p).unwrap();
|
||||
assert_eq!(v["changed"][0]["option"], "thumbnails");
|
||||
assert!(v["warnings"][0].as_str().unwrap().contains("thumbnails"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_runs_export_pdf_with_the_dialog_options() {
|
||||
let (mut app, written) = app();
|
||||
let (mut app, written, opened) = app();
|
||||
app.run("ui.savePdfDialog", json!({"path": "/tmp/out.pdf", "compatibility": "1.5"})).unwrap();
|
||||
set_field(&mut app, "__allArtboards", json!(false));
|
||||
set_field(&mut app, "range", json!("1,3"));
|
||||
set_field(&mut app, "compression.compressText", json!(false));
|
||||
set_field(&mut app, "viewAfterSaving", json!(true));
|
||||
let r = super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "closes after saving");
|
||||
assert_eq!(r["path"], "/tmp/out.pdf");
|
||||
@@ -516,16 +600,20 @@ mod tests {
|
||||
assert!(w[0].1.starts_with(b"%PDF-1.5"), "compatibility reaches the writer");
|
||||
assert_eq!(vectorcraft_pdf::import(&w[0].1).unwrap().artboards.len(), 2, "range 1,3");
|
||||
assert!(!String::from_utf8_lossy(&w[0].1).contains("/FlateDecode"), "uncompressed content");
|
||||
assert_eq!(app.ui.status, "Saved /tmp/out.pdf");
|
||||
assert_eq!(opened.borrow().len(), 1, "View PDF after Saving opens the file once");
|
||||
let url = &opened.borrow()[0].0;
|
||||
assert!(url.starts_with("file:///") && url.ends_with("/tmp/out.pdf"), "the written file as a URL: {url}");
|
||||
assert!(app.ui.status.starts_with("Saved /tmp/out.pdf"), "{}", app.ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_keep_the_dialog_open_and_agents_export_directly() {
|
||||
let (mut app, written) = app();
|
||||
fn warnings_reach_the_status_and_errors_keep_the_dialog_open() {
|
||||
let (mut app, written, opened) = app();
|
||||
app.run("ui.savePdfDialog", json!({"path": "/tmp/w.pdf"})).unwrap();
|
||||
set_field(&mut app, "marks.trim", json!(true));
|
||||
let r = super::super::confirm(&mut app).unwrap();
|
||||
assert!(r["warnings"][0].as_str().unwrap().contains("marks"), "options not applied yet warn: {r}");
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.status.contains("1 note(s)") && app.ui.status.contains("marks"), "{}", app.ui.status);
|
||||
assert!(opened.borrow().is_empty(), "not opened unless asked");
|
||||
app.run("ui.savePdfDialog", json!({"path": "/tmp/bad.pdf"})).unwrap();
|
||||
set_field(&mut app, "__allArtboards", json!(false));
|
||||
set_field(&mut app, "range", json!("7"));
|
||||
@@ -554,7 +642,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn presets_reset_the_settings_and_fields_fall_back_to_defaults() {
|
||||
let (mut app, _) = app();
|
||||
let (mut app, _, _) = app();
|
||||
app.run("ui.savePdfDialog", json!({"compatibility": "1.4", "marks": {"trim": true}})).unwrap();
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
assert_eq!(get(d, "compatibility"), "1.4");
|
||||
@@ -567,4 +655,14 @@ mod tests {
|
||||
assert_eq!(get(&d, "marks.trim"), false);
|
||||
assert!(app.run("ui.savePdfDialog", json!({"compatibility": "1.0"})).is_err(), "bad options are refused");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_labels_read_well() {
|
||||
assert_eq!(option_label("compression.color.abovePpi"), "Compression › Color › Above Ppi");
|
||||
assert_eq!(option_label("compression.compressText"), "Compression › Compress Text");
|
||||
assert_eq!(option_label("thumbnails"), "General › Thumbnails");
|
||||
assert_eq!(option_label("bleed.top"), "Marks and Bleeds › Bleed › Top");
|
||||
assert_eq!(section_of("bleed.top"), 2);
|
||||
assert_eq!(SECTIONS[section_of("standard")], "General");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,14 +159,40 @@ fn run_to_file(app: &mut VectorcraftApp, id: &str, ext: &str, mut params: Value)
|
||||
}
|
||||
|
||||
/// File → Save as PDF: `document.exportPdf` with `params` (the Save PDF dialog's options), written
|
||||
/// to `path` (else a picked or suggested name) → `{path, bytes, warnings}`.
|
||||
/// to `path` (else a picked or suggested name) → `{path, bytes, warnings}`. With
|
||||
/// `viewAfterSaving`, the written file opens in the system viewer (not on the web, which
|
||||
/// downloads it).
|
||||
pub fn export_pdf(app: &mut VectorcraftApp, params: Value) -> Result<Value, String> {
|
||||
let view = params.get("viewAfterSaving").and_then(Value::as_bool).unwrap_or(false);
|
||||
let (path, mut v) = run_to_file(app, "document.exportPdf", "pdf", params)?;
|
||||
app.status(format!("Saved {path}"));
|
||||
if view && app.services.download.is_none() {
|
||||
app.open_url(&file_url(&path));
|
||||
}
|
||||
let warnings: Vec<&str> = v["warnings"].as_array().map(|w| w.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
||||
app.status(match warnings.first() {
|
||||
Some(first) => format!("Saved {path} with {} note(s): {first}", warnings.len()),
|
||||
None => format!("Saved {path}"),
|
||||
});
|
||||
v["path"] = Value::String(path);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// `path` as an absolute `file://` URL for the system opener (bytes other than letters, digits and
|
||||
/// `/-._~:` percent-encoded).
|
||||
fn file_url(path: &str) -> String {
|
||||
let abs = std::path::absolute(path).map_or_else(|_| path.to_string(), |p| p.to_string_lossy().into_owned());
|
||||
let abs = abs.replace('\\', "/");
|
||||
let mut url = String::from(if abs.starts_with('/') { "file://" } else { "file:///" });
|
||||
for b in abs.bytes() {
|
||||
if b.is_ascii_alphanumeric() || b"/-._~:".contains(&b) {
|
||||
url.push(char::from(b));
|
||||
} else {
|
||||
url.push_str(&format!("%{b:02X}"));
|
||||
}
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
/// File → Place… (embed an image or SVG into the active document).
|
||||
pub fn place_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
let f = fileio::detect(name, bytes).ok_or_else(|| format!("can't place `{name}`: not a format VectorCraft reads"))?;
|
||||
@@ -311,6 +337,13 @@ mod tests {
|
||||
assert_eq!(&w[1].1[1..4], b"PNG", "PNG from the given path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn written_files_open_as_file_urls() {
|
||||
let url = super::file_url("/tmp/My Art #1.pdf");
|
||||
assert!(url.starts_with("file:///") && url.ends_with("/tmp/My%20Art%20%231.pdf"), "{url}");
|
||||
assert!(!url.contains('\\'), "{url}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_export_without_path_returns_bytes() {
|
||||
let (mut app, written) = app();
|
||||
|
||||
@@ -169,7 +169,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.export.pdf",
|
||||
"Save as PDF…",
|
||||
"",
|
||||
"{} opens the Save PDF dialog; with params = document.exportPdf options written to path (asked when missing) → {path, bytes, warnings}",
|
||||
"{} opens the Save PDF dialog; with params = document.exportPdf options written to path (asked when missing; viewAfterSaving opens the file) → {path, bytes, warnings}",
|
||||
),
|
||||
("help.about", "About VectorCraft", "", "{}"),
|
||||
("help.commandPalette", "Search Commands…", "Cmd+Shift+/", "{}"),
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
| `ui.resize` / `ui.focus` | | |
|
||||
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`). `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
|
||||
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security). `ui.dialog.confirm` writes `path` (else asks for one) and returns `{path, bytes, warnings}` |
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
|
||||
|
||||
Swatch editors: `engine.execute` with `ui.swatchOptions {name}` opens the `swatchOptions` dialog for a colour swatch
|
||||
(fields `name`, `spot`, `global`, `mode`: `gray`/`rgb`/`hsb`/`lab`/`cmyk`/`web`, `color`: `"#rrggbb"` or a colour object,
|
||||
|
||||
+3
-1
@@ -149,12 +149,14 @@ Long-tail commands:
|
||||
{"name":"run_command","arguments":{"command":"object.align","params":{"align":"left"}}}
|
||||
{"name":"run_command","arguments":{"command":"object.group"}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/art.pdf","range":"1, 3","compatibility":"1.5"}}}
|
||||
{"name":"run_command","arguments":{"command":"document.pdfSettings","params":{"marks":{"trim":true},"includeDocument":true}}}
|
||||
```
|
||||
|
||||
PDF files take the Save PDF dialog's options: `preset`, `standard`, `compatibility`, the General toggles and the
|
||||
`compression`, `marks`, `bleed`, `output`, `advanced` and `security` sections (`list_commands` with filter `exportPdf`
|
||||
documents every field); `export` with format `pdf` takes them in `options`. `document.exportPdf` returns `warnings`:
|
||||
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.
|
||||
|
||||
Drive a tool like a mouse:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user