Files
vectorcraft/crates/ui-egui/src/print.rs
T
bflatasticandClaude Opus 5.5 0428e56342 M4.92: Print to a PostScript file
file.print {format: "postscript"} (the default for a .ps path) writes the
print job as Level 3 or 2 (level) PostScript instead of a PDF: DSC comments
with %%Pages the page count, one page per sheet with its paper size,
printer's marks in Registration, separations naming their ink (%%PlateColor)
with its halftone screen, a fixed flatness and film negatives. Transparency
is flattened first with flattenerPreset. The answer says which format was
written. In the app, a PostScript job is always saved, never sent to the
printer (the Print dialog's own options are unchanged).

The PDF print's layout is exposed as vectorcraft_pdf::plan (the PDF writer
uses it too); vectorcraft_eps::print draws its sheets with the EPS writer's
document walk, each sheet once however many copies print.

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

97 lines
4.5 KiB
Rust

//! Printing through the platform. The host app installs a [`PrintService`] ([`crate::Services`]):
//! the desktop hands the job to the system's print spooler, the web to the browser's print dialog.
//! `file.print` with settings sends the engine's print-ready PDF (`file.print` in the engine) to
//! it, or saves the PDF when asked to (`toFile`, `path`) or when there is no print service; a
//! PostScript file (`format: "postscript"`, or a `.ps` path) is always saved. `print.printers`
//! lists the printers.
use serde::Serialize;
use serde_json::{Value, json};
use vectorcraft_engine::cmd::fileio;
use crate::{VectorcraftApp, io};
/// A printer the system knows.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Printer {
pub name: String,
/// The system's default printer.
pub default: bool,
}
/// A print job: a print-ready PDF, every copy and page in it.
pub struct PrintJob<'a> {
/// The printer; `None` = the system's default (the web: the browser's print dialog).
pub printer: Option<&'a str>,
/// The job's name in the print queue.
pub title: &'a str,
pub pdf: &'a [u8],
}
/// The platform's printing ([`crate::Services::print`]).
pub trait PrintService {
/// The printers the system knows (none: only the default one, or the browser's dialog).
fn printers(&mut self) -> Vec<Printer>;
/// Send `job` to its printer → what happened, for the status bar.
fn print(&mut self, job: &PrintJob) -> Result<String, String>;
/// Whether Setup… can open the system's printer settings.
fn has_setup(&self) -> bool {
false
}
/// Setup…: open the system's settings of `printer` (`None`: the printers).
fn setup(&mut self, _printer: Option<&str>) -> Result<(), String> {
Err("there are no printer settings to open here".into())
}
}
/// `print.printers`: the system's printers, and what the print service offers.
pub fn printers(app: &mut VectorcraftApp) -> Value {
let (printers, setup) = app.services.print.as_mut().map(|s| (s.printers(), s.has_setup())).unwrap_or_default();
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
/// (`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> {
let s = |k: &str| p.get(k).and_then(Value::as_str).map(str::to_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"] {
if let Some(v) = p.get(k).filter(|v| !v.is_null()) {
q[k] = v.clone();
}
}
if postscript {
// The path is taken off before the engine runs: it can't tell the format from it.
q["format"] = json!("postscript");
}
if to_file || app.services.print.is_none() {
if let Some(path) = s("path") {
q["path"] = json!(path);
}
let (path, mut v) = io::run_to_file(app, "file.print", if postscript { "ps" } else { "pdf" }, q)?;
io::report_saved(app, &path, &v);
if !to_file {
app.ui.status.push_str(" (printing isn't available here: the job was saved as a PDF)");
}
v["path"] = json!(path);
v["printed"] = json!(false);
return Ok(v);
}
let mut v = app.session.execute("file.print", &q).map_err(|e| e.to_string())?;
let data = v.as_object_mut().and_then(|o| o.remove("dataBase64"));
let pdf = data.as_ref().and_then(Value::as_str).and_then(vectorcraft_format::base64_decode).ok_or("the print job has no data")?;
let title = app.session.active().map(|d| d.title()).unwrap_or_default();
let printer = s("printer").filter(|n| !n.is_empty());
let service = app.services.print.as_mut().ok_or("no print service")?;
let done = service.print(&PrintJob { printer: printer.as_deref(), title: &title, pdf: &pdf })?;
app.status(done);
v["printer"] = json!(printer);
v["printed"] = json!(true);
Ok(v)
}