Print: Print, Print Preview and Print Setup take a print preset by name

file.print, print.preview and print.setup took print settings only as
settings, so an agent had to read a preset with print.presets.list and
copy its settings into the call. They now take preset, the name of
[Default] or of a saved print preset in any case. The preset's settings
are used instead of the document's, settings applies over them, and
print.setup saves them with the document as one undo step. An unknown
name is a parameter error that lists the presets. The app's file.print
with params passes preset on to the engine.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 13:49:49 +07:00
committed by bflatastic
parent d5e44b321e
commit a285efda46
9 changed files with 137 additions and 18 deletions
+25 -7
View File
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Print Setup",
[],
None,
"{settings?: {copies: 1 (1–999), collate: true, reverse, artboards: all|range|ignore (all the art as one page), range: \"1-3, 5\" (1-based, with artboards: range), skipBlank (leave out artboards with no art that prints), media: letter|legal|tabloid|a3|a4|a5|b4|b5|custom, width, height (custom paper, 72–14400 pt), orientation: portrait|landscape|portraitFlipped|landscapeFlipped, autoRotate: true (turn the paper to each artboard; orientation ignored), transverse (the page a quarter turn on the paper), printLayers: visiblePrintable|visible|all (template layers never print), placement: {origin: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight (the point of the printed area, the artboard with its bleed and marks, on the same point of the imageable area), x, y (pt, right and down)}, scaling: none|fit|custom|tileFull|tileImageable, scale: {width: 100, height: 100} (% with custom and tiling), overlap: 0 (pt between tiles), tileRange: \"\" (1-based tiles across then down; empty: all), margin: 0 (pt the device can't print around the paper: the imageable area is inside it), marks: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6} (as document.exportPdf, at the paper's scale; page information adds the tile and the ink), bleed: {useDocument: true (the document's bleed, document.setup), top, bottom, left, right} (pt), output: {mode: composite|separations (one grey page per ink that prints), emulsion: up|down (down mirrors), image: positive|negative, spotsToProcess, inks: [{name: \"Cyan\"|…|a spot swatch, print: true, frequency (lpi, default 60), angle (default C 15, M 75, Y 0, K 45, spots 45)}]}, graphics: {autoFlatness: true, flatness: 1 (0.2–100), fonts: none|subset|complete}, color: {intent: perceptual|relativeColorimetric|saturation|absoluteColorimetric, preserveNumbers: true (CMYK colours keep their values), profile: \"\" (the printer profile, an RGB or CMYK one: composite PDF colours are converted to it with the intent, separations separate with a CMYK one; empty: as they are)}, advanced: {printAsBitmap (composite pages print as one image each, at the document's raster effects resolution), overprints: preserve|discard|simulate (composite output; preserve writes them to overprint (/OP), simulate prints them as Overprint Preview shows them; separations always keep them), flattenerPreset: \"\" (a flattener preset, built-in or saved: transparency is flattened before printing; empty: it prints live; PostScript defaults to medium)}, tileOrigin: {placed: false, x, y} (where the Print Tiling tool put the pages: print.tiling.set)}} store the print settings with the document, over the ones it has (null keeps a value), as one undo step; no settings → {settings} (the current ones, defaults if never set up)",
"{preset?: name ([Default] or a saved print preset, any case: print.presets.list; its settings replace the document's, as choosing it in the Print dialog does), settings?: {copies: 1 (1–999), collate: true, reverse, artboards: all|range|ignore (all the art as one page), range: \"1-3, 5\" (1-based, with artboards: range), skipBlank (leave out artboards with no art that prints), media: letter|legal|tabloid|a3|a4|a5|b4|b5|custom, width, height (custom paper, 72–14400 pt), orientation: portrait|landscape|portraitFlipped|landscapeFlipped, autoRotate: true (turn the paper to each artboard; orientation ignored), transverse (the page a quarter turn on the paper), printLayers: visiblePrintable|visible|all (template layers never print), placement: {origin: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight (the point of the printed area, the artboard with its bleed and marks, on the same point of the imageable area), x, y (pt, right and down)}, scaling: none|fit|custom|tileFull|tileImageable, scale: {width: 100, height: 100} (% with custom and tiling), overlap: 0 (pt between tiles), tileRange: \"\" (1-based tiles across then down; empty: all), margin: 0 (pt the device can't print around the paper: the imageable area is inside it), marks: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6} (as document.exportPdf, at the paper's scale; page information adds the tile and the ink), bleed: {useDocument: true (the document's bleed, document.setup), top, bottom, left, right} (pt), output: {mode: composite|separations (one grey page per ink that prints), emulsion: up|down (down mirrors), image: positive|negative, spotsToProcess, inks: [{name: \"Cyan\"|…|a spot swatch, print: true, frequency (lpi, default 60), angle (default C 15, M 75, Y 0, K 45, spots 45)}]}, graphics: {autoFlatness: true, flatness: 1 (0.2–100), fonts: none|subset|complete}, color: {intent: perceptual|relativeColorimetric|saturation|absoluteColorimetric, preserveNumbers: true (CMYK colours keep their values), profile: \"\" (the printer profile, an RGB or CMYK one: composite PDF colours are converted to it with the intent, separations separate with a CMYK one; empty: as they are)}, advanced: {printAsBitmap (composite pages print as one image each, at the document's raster effects resolution), overprints: preserve|discard|simulate (composite output; preserve writes them to overprint (/OP), simulate prints them as Overprint Preview shows them; separations always keep them), flattenerPreset: \"\" (a flattener preset, built-in or saved: transparency is flattened before printing; empty: it prints live; PostScript defaults to medium)}, tileOrigin: {placed: false, x, y} (where the Print Tiling tool put the pages: print.tiling.set)}} store the print settings with the document, over the ones it has or the preset's (null keeps a value), as one undo step; no settings or preset → {settings} (the current ones, defaults if never set up)",
has_doc,
setup
),
@@ -31,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Print Preview",
[],
None,
"{settings?: {…settings of print.setup} (over the document's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, type that doesn't fit its frame, options not applied yet…), settings} without printing",
"{preset?: name (as print.setup's: its settings instead of the document's), settings?: {…settings of print.setup} (over the document's or the preset's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, type that doesn't fit its frame, options not applied yet…), settings} without printing",
has_doc,
preview
),
@@ -40,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Print",
[],
Some("Cmd+P"),
"{settings?: {…settings of print.setup} (over the document's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device",
"{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device",
has_doc,
print
),
@@ -57,6 +57,23 @@ pub(crate) fn settings(cmd: &str, doc: &Document, p: &Value) -> Result<PrintSett
settings_over(cmd, saved(doc), p)
}
/// The settings `p` asks for: those of the print preset `p.preset` names ([Default] or a saved
/// one, any case), else those saved with `doc`, with `p.settings` over them, checked.
fn preset_or_saved(cmd: &str, s: &Session, doc: &Document, p: &Value) -> Result<PrintSettings> {
let base = match p.get("preset") {
None | Some(Value::Null) => saved(doc),
Some(Value::String(name)) => {
let set = s.print_preset_settings(name).ok_or_else(|| {
let names: Vec<String> = s.print_presets().into_iter().map(|q| q.name).collect();
bad(cmd, format!("no print preset named `{name}` (presets: {})", names.join(", ")))
})?;
to_json(&set)?
}
Some(_) => return Err(bad(cmd, "preset must be the name of a print preset")),
};
settings_over(cmd, base, p)
}
/// The settings `v` (JSON) with `p.settings` over them, checked.
pub(crate) fn settings_over(cmd: &str, mut v: Value, p: &Value) -> Result<PrintSettings> {
match p.get("settings") {
@@ -75,8 +92,9 @@ pub(crate) fn to_json(set: &PrintSettings) -> Result<Value> {
fn setup(s: &mut Session, p: &Value) -> Result<Value> {
let doc = &s.doc()?.doc;
let set = to_json(&settings(SETUP, doc, p)?)?;
if p.get("settings").is_some_and(|v| !v.is_null()) && doc.print_setup.as_ref() != Some(&set) {
let set = to_json(&preset_or_saved(SETUP, s, doc, p)?)?;
let given = |k: &str| p.get(k).is_some_and(|v| !v.is_null());
if (given("settings") || given("preset")) && doc.print_setup.as_ref() != Some(&set) {
let stored = set.clone();
s.edit("Print Setup", |d, _| {
d.print_setup = Some(stored);
@@ -89,7 +107,7 @@ fn setup(s: &mut Session, p: &Value) -> Result<Value> {
fn preview(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "print.preview";
let doc = &s.doc()?.doc;
let set = settings(C, doc, p)?;
let set = preset_or_saved(C, s, doc, p)?;
super::printadvanced::flattener(C, &set, &s.prefs.flattener_presets)?;
let mut pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?;
pv.warnings.extend(type_warnings(doc, set.print_layers, &areas(&pv)));
@@ -148,7 +166,7 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
}
let postscript = to_postscript(p)?;
let doc = &s.doc()?.doc;
let mut set = settings(PRINT, doc, p)?;
let mut set = preset_or_saved(PRINT, s, doc, p)?;
if postscript {
// PostScript has no transparency: `flattenerPreset`, else the Advanced preset, else medium.
let given = str_param(p, "flattenerPreset").filter(|n| !n.trim().is_empty());
+60
View File
@@ -66,6 +66,66 @@ fn print_writes_the_saved_setup_with_overrides() {
let _ = std::fs::remove_file(&path);
}
/// A document with four 300 × 200 artboards, set up to print two copies, and a saved print preset
/// "Sheet" that prints artboard 4 only.
fn four_artboards_and_a_preset() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200, "artboards": 4})).unwrap();
s.execute("print.setup", &json!({"settings": {"copies": 2}})).unwrap();
s.execute("print.presets.save", &json!({"name": "Sheet", "settings": {"artboards": "range", "range": "4"}})).unwrap();
s
}
#[test]
fn print_takes_a_print_preset_by_name() {
let mut s = four_artboards_and_a_preset();
// The preset's settings stand in for the document's: one copy of artboard 4.
let (bytes, v) = print(&mut s, json!({"preset": "sheet"}));
assert_eq!((v["pages"].clone(), vectorcraft_pdf::info(&bytes, None).unwrap().pages.len()), (json!(1), 1));
let (_, v) = print(&mut s, json!({"preset": "Sheet", "settings": {"copies": 3}}));
assert_eq!(v["pages"], 3, "settings apply over the preset's");
let (_, v) = print(&mut s, json!({"preset": "[Default]"}));
assert_eq!(v["pages"], 4, "[Default]: every artboard, one copy");
assert_eq!(s.execute("print.setup", &json!({})).unwrap()["settings"]["copies"], 2, "printing doesn't change the saved setup");
// A PostScript file too.
let v = s.execute("file.print", &json!({"preset": "Sheet", "format": "postscript"})).unwrap();
assert_eq!(v["pages"], 1);
}
#[test]
fn an_unknown_print_preset_is_refused_with_the_presets() {
let mut s = four_artboards_and_a_preset();
for c in ["file.print", "print.preview", "print.setup"] {
// Agents (MCP, the control channel) may pass `preset`.
s.check_params(c, &json!({"preset": "Sheet", "settings": {}})).unwrap();
let e = s.execute(c, &json!({"preset": "Nope"})).unwrap_err();
assert!(matches!(e, EngineError::BadParams { .. }), "{c}: {e}");
assert!(e.to_string().contains("no print preset named `Nope` (presets: [Default], Sheet)"), "{c}: {e}");
assert!(matches!(s.execute(c, &json!({"preset": 4})), Err(EngineError::BadParams { .. })), "{c}");
}
assert_eq!(s.doc().unwrap().doc.print_setup.as_ref().unwrap()["copies"], 2);
}
#[test]
fn setup_with_a_preset_stores_its_settings() {
let mut s = four_artboards_and_a_preset();
let undo = s.doc().unwrap().history.undo.len();
let v = s.execute("print.setup", &json!({"preset": "Sheet"})).unwrap();
let got = (v["settings"]["copies"].clone(), v["settings"]["artboards"].clone(), v["settings"]["range"].clone());
assert_eq!(got, (json!(1), json!("range"), json!("4")));
let stored = s.doc().unwrap().doc.print_setup.clone().unwrap();
assert_eq!(stored, v["settings"]);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
// The same preset again is no new step; settings apply over it.
s.execute("print.setup", &json!({"preset": "Sheet"})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
let v = s.execute("print.setup", &json!({"preset": "Sheet", "settings": {"collate": false}})).unwrap();
assert_eq!((v["settings"]["range"].clone(), v["settings"]["collate"].clone()), (json!("4"), json!(false)));
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.print_setup.as_ref().unwrap()["copies"], 2);
}
#[test]
fn preview_lists_pages_tiles_inks_and_warnings() {
let (mut s, _) = session();
+14
View File
@@ -52,6 +52,20 @@ fn pages_place_the_artboard_on_the_trim_box() {
}
}
#[test]
fn a_print_preset_lays_out_its_settings_with_settings_over_them() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200, "artboards": 4})).unwrap();
s.execute("print.setup", &json!({"settings": {"media": "a4"}})).unwrap();
s.execute("print.presets.save", &json!({"name": "Sheet", "settings": {"artboards": "range", "range": "4"}})).unwrap();
let v = s.execute("print.preview", &json!({"preset": "Sheet", "settings": {"artboards": "range", "range": "3"}})).unwrap();
assert_eq!((v["pages"].clone(), v["sheets"][0]["artboard"].clone()), (json!(1), json!(2)), "{v}");
assert_eq!(v["settings"]["media"], "letter", "the preset's paper, not the document's");
let v = s.execute("print.preview", &json!({"preset": "Sheet"})).unwrap();
assert_eq!((v["pages"].clone(), v["sheets"][0]["artboard"].clone()), (json!(1), json!(3)));
assert_eq!(s.doc().unwrap().doc.print_setup.as_ref().unwrap()["media"], "a4", "the preview keeps nothing");
}
/// Point type at (`x`, `y`) in `family` → its id.
fn type_in(s: &mut Session, x: f64, y: f64, family: &str) -> u64 {
s.execute("text.create", &json!({"x": x, "y": y, "text": "Ab", "font": family, "style": "Regular"})).unwrap()["id"].as_u64().unwrap()
+2 -1
View File
@@ -1932,12 +1932,13 @@ proptest! {
// ---------- print presets files ----------
/// Import print presets from `data`, then lay out (and print, when short) the rich document with
/// each one imported.
/// each one imported, by its settings and by its name.
fn print_presets(what: &str, data: &str) -> Result<(), TestCaseError> {
survive_library(what, "print.presets.import", data, |s, r| {
let list = no_panic(s.execute("print.presets.list", &json!({}))).unwrap_or_default();
for name in r["imported"].as_array().into_iter().flatten() {
let Some(p) = list["presets"].as_array().into_iter().flatten().find(|p| &p["name"] == name) else { continue };
let _ = no_panic(s.execute("print.preview", &json!({ "preset": name })));
let settings = json!({ "settings": p["settings"] });
let pages = no_panic(s.execute("print.preview", &settings)).ok().and_then(|v| v["pages"].as_u64());
let _ = no_panic(s.execute("print.setup", &settings));
@@ -1,5 +1,9 @@
//! Print presets in the UI: the Print dialog's preset list and Save Preset…, and Edit → Print
//! Presets (New…, Edit…, Delete, Import…, Export…) with its preset editor.
//! Presets (New…, Edit…, Delete, Import…, Export…) with its preset editor, and `file.print` with
//! a preset's name.
use std::cell::RefCell;
use std::rc::Rc;
use serde_json::{Value, json};
use vectorcraft_engine::Session;
@@ -7,7 +11,7 @@ use vectorcraft_engine::cmd::printpresets::DEFAULT_PRESET;
use super::print::{KIND, PRESET_KIND, SECTIONS};
use super::print_presets::{self, Action};
use crate::{VectorcraftApp, theme};
use crate::{Services, VectorcraftApp, theme};
fn frame(app: &mut VectorcraftApp) {
let ctx = egui::Context::default();
@@ -77,6 +81,22 @@ fn the_print_dialog_saves_and_loads_presets() {
assert_eq!(app.session.prefs.print_presets.len(), 1);
}
#[test]
fn file_print_with_params_prints_a_preset_by_name() {
let written: Rc<RefCell<Vec<Vec<u8>>>> = Rc::default();
let w = written.clone();
let write = move |_: &str, b: &[u8]| {
w.borrow_mut().push(b.to_vec());
Ok(())
};
let mut app = VectorcraftApp::new(Session::new(), Services { write: Some(Box::new(write)), ..Default::default() });
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 4})).unwrap();
app.run("print.presets.save", json!({"name": "Sheet", "settings": {"artboards": "range", "range": "4"}})).unwrap();
let r = app.run("file.print", json!({"path": "/tmp/sheet.pdf", "preset": "Sheet"})).unwrap();
assert_eq!(r["pages"], 1, "{r}");
assert_eq!(vectorcraft_pdf::info(&written.borrow()[0], None).unwrap().pages.len(), 1);
}
#[test]
fn the_manager_adds_edits_and_deletes_presets_through_the_editor() {
// Edit → Print Presets works without a document too.
+1 -1
View File
@@ -812,7 +812,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"file.print",
"Print…",
"Cmd+P",
"{} open the Print dialog (dialog `print`: the print.setup settings, preset (setting it loads that print preset; Save Preset… saves them as one), printer, toFile; Print keeps the settings with the document and prints, Done (discard: true) only keeps them); with params {settings? (over the document's), printer? (print.printers name; \"\" the default), toFile?, path?, format?: pdf|postscript, level?, flattenerPreset? (as engine file.print)} print without it: the print-ready PDF (engine file.print) goes to the printer → {pages, printer, printed: true, warnings}; with toFile, a path or no printing here (no print service) it is saved as a PDF at path (else a picked file, the web downloads it) → {path, pages, printed: false, warnings}; a PostScript file (format postscript, or a .ps path) is always saved that way",
"{} open the Print dialog (dialog `print`: the print.setup settings, preset (setting it loads that print preset; Save Preset… saves them as one), printer, toFile; Print keeps the settings with the document and prints, Done (discard: true) only keeps them); with params {preset? (a print preset's name: its settings instead of the document's), settings? (over the document's or the preset's), printer? (print.printers name; \"\" the default), toFile?, path?, format?: pdf|postscript, level?, flattenerPreset? (as engine file.print)} print without it: the print-ready PDF (engine file.print) goes to the printer → {pages, printer, printed: true, warnings}; with toFile, a path or no printing here (no print service) it is saved as a PDF at path (else a picked file, the web downloads it) → {path, pages, printed: false, warnings}; a PostScript file (format postscript, or a .ps path) is always saved that way",
),
(
"print.printers",
+5 -4
View File
@@ -58,9 +58,10 @@ pub fn printers(app: &mut VectorcraftApp) -> Value {
json!({ "printers": printers, "service": app.services.print.is_some(), "setup": setup })
}
/// `file.print` with settings: print the active document with `settings` (over the document's)
/// on `printer` (default: the system's default), or save the job as a PDF at `path` (else a picked
/// file, the web downloads it) with `toFile`, a `path` or no print service. A PostScript file
/// `file.print` with settings: print the active document with `settings` (over the document's, or
/// over those of the print preset `preset` names) on `printer` (default: the system's default), or
/// save the job as a PDF at `path` (else a picked file, the web downloads it) with `toFile`, a
/// `path` or no print service. A PostScript file
/// (`format: "postscript"` or a `.ps` path, at `level`, flattened with `flattenerPreset`) is saved
/// the same way.
pub fn run(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
@@ -68,7 +69,7 @@ pub fn run(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let postscript = s("format").as_deref() == Some("postscript") || s("path").is_some_and(|path| fileio::extension(&path) == "ps");
let to_file = p.get("toFile").and_then(Value::as_bool).unwrap_or(false) || s("path").is_some() || postscript;
let mut q = json!({});
for k in ["settings", "format", "level", "flattenerPreset"] {
for k in ["preset", "settings", "format", "level", "flattenerPreset"] {
if let Some(v) = p.get(k).filter(|v| !v.is_null()) {
q[k] = v.clone();
}
+7 -3
View File
@@ -2749,7 +2749,8 @@ answers what it would print without writing it: `{pages (copies included), sheet
height, orientation, scale}] (one copy, in order), tiles: [{artboard, columns, rows, printed, tiles}] (document
space), inks: [{name, spot, print, frequency, angle}], warnings, settings}`. `print.setup {settings?}` saves the
settings with the document (one undo step, kept in the native file as `print_setup`); both other commands start from
the saved settings, with `settings` over them (`null` keeps a value). `list_commands` with filter `print.setup`
the saved settings, or from a print preset's with `preset` (see Print presets), with `settings` over them (`null` keeps
a value). `list_commands` with filter `print.setup`
documents every field:
- General: `copies` (1–999), `collate`, `reverse`; `artboards: all|range|ignore` with `range: "1-3, 5"` (ignore: all
@@ -3017,11 +3018,14 @@ preset?, settings?}` creates or changes a saved preset (starting from its own se
`settings` over them) and `print.presets.delete {name}` deletes one; `[Default]` can be neither changed nor deleted.
`print.presets.export {names?, path?}` writes them as a `.vcprintpresets` JSON file (without `path` it returns `data`),
and `print.presets.import {path? | data? | dataBase64?, replace?}` adds a file's presets (a name in use gets a number
unless `replace`). Saved presets live with the preferences. To print with a preset, pass its `settings` to
`print.setup`, `print.preview` or `file.print`.
unless `replace`). Saved presets live with the preferences. `print.setup`, `print.preview` and `file.print` take
`preset`, the name of `[Default]` or of a saved preset in any case: the preset's settings are used instead of the
document's, with `settings` over them, and `print.setup {preset}` saves them with the document. An unknown name is
refused with the list of presets.
```json
{"name":"run_command","arguments":{"command":"print.presets.save","params":{"name":"Posters","settings":{"scaling":"tileImageable","overlap":18,"marks":{"trim":true}}}}}
{"name":"run_command","arguments":{"command":"file.print","params":{"preset":"Posters","path":"/tmp/posters.pdf"}}}
```
## EPS and PostScript import
+1
View File
@@ -622,6 +622,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it.
- Desktop print transport uses `craft-print` for CUPS and modern Windows. Sheet composition, copies, separations and colour management remain in VectorCraft; Windows 7 retains WMI and the installed PDF viewer. The print-preview and physical-device fidelity gaps remain, so print scores and hours are unchanged.
- A PDF, PDF-compatible `.ai` or `.ait` opened from its pages or its editing copy opens with the File Info that its XMP packet and its document information dictionary give: the author, description, keywords, copyright status, notice and URL, and the created and modified dates, the XMP packet's value where both give one; the title is not changed (`crates/pdf/src/tests_file_info.rs`, `crates/engine/src/tests_fileinfo.rs`).
- `file.print`, `print.preview` and `print.setup` take a print preset by name (`preset`: `[Default]` or a saved preset, in any case): its settings are used instead of the document's, with `settings` over them, `print.setup` saves them with the document, and an unknown name is refused with the list of presets.
- Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
- On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.