Files
vectorcraft/crates/engine/src/cmd/print.rs
T
bflatasticandClaude Opus 5.5 a09cbe58c8 M4.83: overprinting fills and strokes overprint in PDF
Advanced > Overprint: Preserve (the default; document.exportPdf
advanced.overprint) writes each overprinting fill and stroke (an object's
own, a character's, a type object's) with a graphics state of /OP true
/op true /OPM 1 (/OPM 0 in PDF/A); Discard writes none. White ones knock out
when Document Setup's Discard White Overprint is on, as in print. Mask art
never overprints. File > Print's composite PDF honours Print > Advanced >
Overprints: Preserve the same way (separations keep multiplying plates).

Each overprinting fill or stroke is drawn as a form marked to overprint
(crates/pdf/src/forms.rs); after writing, its content starts by setting the
overprinting graphics state, and the PDF/X self-check runs on the result.
The engine's "overprint is not written to PDF yet" warning and print's
"overprints print as knockouts in composite output" warning are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:39:28 +07:00

174 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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> {
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();
}
// 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 }))
}