mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
- a story is read once however many text objects name it; the text objects, their pairs with the page's type, the pieces of a line and a story's size are capped, and over a cap the layers aren't used - the ASCII85 of the text document is bounded before it is decoded - joining the pieces of lines that interleave no longer removes the wrong pieces - a run length too large for the text saturates instead of overflowing - a dictionary left open at the end of a layer, and layers nested deeper than are read, make the file one that comes in as its page (they were silently dropped) - a file whose editing data can't be decoded says so, and the notes about art that was too much or too far away count as losses, so writing over the source asks first - the private data of a .ai is inflated under a cap of its own (the PDF reader's decoder has none) - the other writers (PDF, selection, office, print) ask before replacing a source that lost things, and a recovered copy remembers that Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
180 lines
11 KiB
Rust
180 lines
11 KiB
Rust
//! File → Print as a print-ready PDF or a PostScript file (`file.print`), what it would print
|
||
//! (`print.preview`) and the print settings saved with the document (`print.setup`,
|
||
//! [`vectorcraft_doc::Document::print_setup`]).
|
||
|
||
use serde::Deserialize;
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_doc::Document;
|
||
use vectorcraft_eps::{Level, PrintJob, PrintPage};
|
||
use vectorcraft_pdf::{PrintOptions, PrintSettings};
|
||
|
||
use super::fileio::pdf::{merge, pdf_error};
|
||
use super::fileio::{extension, write_or_return};
|
||
use super::*;
|
||
|
||
const SETUP: &str = "print.setup";
|
||
|
||
pub fn specs() -> Vec<CommandSpec> {
|
||
vec![
|
||
cmd!(
|
||
"print.setup",
|
||
"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)",
|
||
has_doc,
|
||
setup
|
||
),
|
||
cmd!(
|
||
query "print.preview",
|
||
"Print Preview",
|
||
[],
|
||
None,
|
||
"{settings?: {…print.setup settings} (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, options not applied yet…), settings} without printing",
|
||
has_doc,
|
||
preview
|
||
),
|
||
cmd!(
|
||
"file.print",
|
||
"Print",
|
||
[],
|
||
Some("Cmd+P"),
|
||
"{settings?: {…print.setup settings} (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)} 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
|
||
),
|
||
]
|
||
}
|
||
|
||
/// The print settings saved with `doc` (defaults if never set up) as JSON.
|
||
fn saved(doc: &Document) -> Value {
|
||
doc.print_setup.clone().unwrap_or_else(|| serde_json::to_value(PrintSettings::default()).unwrap_or_default())
|
||
}
|
||
|
||
/// The settings of `doc` with `p.settings` over them, checked.
|
||
pub(crate) fn settings(cmd: &str, doc: &Document, p: &Value) -> Result<PrintSettings> {
|
||
settings_over(cmd, saved(doc), 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") {
|
||
None | Some(Value::Null) => {}
|
||
Some(over @ Value::Object(_)) => merge(&mut v, over),
|
||
Some(_) => return Err(bad(cmd, "settings must be an object")),
|
||
}
|
||
let set = PrintSettings::deserialize(&v).map_err(|e| bad(cmd, format!("print settings: {e}")))?;
|
||
set.check().map_err(|e| pdf_error(cmd, e))?;
|
||
Ok(set)
|
||
}
|
||
|
||
pub(crate) fn to_json(set: &PrintSettings) -> Result<Value> {
|
||
serde_json::to_value(set).map_err(|e| EngineError::Other(e.to_string()))
|
||
}
|
||
|
||
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 stored = set.clone();
|
||
s.edit("Print Setup", |d, _| {
|
||
d.print_setup = Some(stored);
|
||
Ok(())
|
||
})?;
|
||
}
|
||
Ok(json!({ "settings": set }))
|
||
}
|
||
|
||
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)?;
|
||
super::printadvanced::flattener(C, &set, &s.prefs.flattener_presets)?;
|
||
let pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?;
|
||
let mut v = serde_json::to_value(pv).map_err(|e| EngineError::Other(e.to_string()))?;
|
||
v["settings"] = to_json(&set)?;
|
||
Ok(v)
|
||
}
|
||
|
||
const PRINT: &str = "file.print";
|
||
|
||
/// Whether `file.print` writes PostScript: `format`, else a `.ps` path.
|
||
fn to_postscript(p: &Value) -> Result<bool> {
|
||
match str_param(p, "format") {
|
||
None => Ok(str_param(p, "path").is_some_and(|path| extension(path) == "ps")),
|
||
Some("postscript") => Ok(true),
|
||
Some("pdf") => Ok(false),
|
||
Some(f) => Err(bad(PRINT, format!("format `{f}`: pdf or postscript"))),
|
||
}
|
||
}
|
||
|
||
fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||
if let Some(path) = str_param(p, "path") {
|
||
super::fileio::check_not_lossy_overwrite(s.doc()?, path, p, PRINT)?;
|
||
}
|
||
let postscript = to_postscript(p)?;
|
||
let doc = &s.doc()?.doc;
|
||
let mut set = settings(PRINT, 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());
|
||
let preset = given.or(Some(set.advanced.flattener_preset.as_str()).filter(|n| !n.trim().is_empty())).unwrap_or("medium");
|
||
set.advanced.flattener_preset = preset.to_string();
|
||
}
|
||
// Placed documents print their files' art.
|
||
let (full, _) = super::place::document::full_documents(doc);
|
||
let doc = &*full;
|
||
// Raster effects print as images at the document's raster effects resolution.
|
||
let flat = super::rasterfx::flatten_raster_effects(doc);
|
||
let doc = flat.as_ref().unwrap_or(doc);
|
||
// Print as Bitmap and the flattener preset (Advanced) render and flatten it first.
|
||
let prepared = super::printadvanced::prepare(PRINT, doc, &set, &s.prefs.flattener_presets)?;
|
||
let path = str_param(p, "path");
|
||
if !postscript {
|
||
let r = vectorcraft_pdf::print(prepared.as_ref().unwrap_or(doc), &PrintOptions { settings: set, ..Default::default() })
|
||
.map_err(|e| pdf_error(PRINT, e))?;
|
||
return write_or_return(path, &r.bytes, json!({ "pages": r.pages, "format": "pdf", "warnings": r.warnings }));
|
||
}
|
||
let level = match p.get("level").filter(|v| !v.is_null()) {
|
||
Some(v) => {
|
||
let t = super::fileio::dxf::text(v);
|
||
Level::from_id(&t).ok_or_else(|| bad(PRINT, format!("level `{t}`: 2 or 3")))?
|
||
}
|
||
None => Level::default(),
|
||
};
|
||
let mut warnings = vec![];
|
||
let bitmap = set.advanced.print_as_bitmap && set.output.mode == vectorcraft_pdf::OutputMode::Composite;
|
||
if !bitmap && doc.layers.iter().any(|l| l.shows_transparency()) {
|
||
warnings.push("transparency is flattened into opaque art and images (PostScript has none): see flattenerPreset".to_string());
|
||
}
|
||
if !set.color.profile.trim().is_empty() && set.output.mode == vectorcraft_pdf::OutputMode::Composite {
|
||
warnings.push("the printer profile converts the colours of PDF output: PostScript prints the document's colours".to_string());
|
||
}
|
||
let flatness = (!set.graphics.auto_flatness).then_some(set.graphics.flatness);
|
||
let plan = vectorcraft_pdf::plan(prepared.as_ref().unwrap_or(doc), &PrintOptions { settings: set, postscript: true, ..Default::default() })
|
||
.map_err(|e| pdf_error(PRINT, e))?;
|
||
let pages: Vec<PrintPage> = plan
|
||
.sheets
|
||
.iter()
|
||
.map(|sh| PrintPage {
|
||
doc: sh.plate.and_then(|i| plan.plates.get(i)).unwrap_or(&plan.doc),
|
||
size: sh.size,
|
||
view: sh.view,
|
||
window: sh.window,
|
||
place: sh.place,
|
||
area: sh.area,
|
||
marks: sh.marks.as_ref(),
|
||
negative: sh.negative,
|
||
ink: sh.ink.as_ref().map(|i| (i.name.as_str(), i.frequency, i.angle)),
|
||
})
|
||
.collect();
|
||
let job = PrintJob { level, title: plan.title.clone(), created: plan.created, flatness };
|
||
let out = vectorcraft_eps::print(&pages, &plan.order, &job).map_err(|e| bad(PRINT, e))?;
|
||
for w in plan.warnings.iter().chain(&out.warnings) {
|
||
if !warnings.contains(w) {
|
||
warnings.push(w.clone());
|
||
}
|
||
}
|
||
write_or_return(path, &out.bytes, json!({ "pages": plan.order.len(), "format": "postscript", "warnings": warnings }))
|
||
}
|