Files
vectorcraft/crates/ui-egui/src/print.rs
T
dotCooCoo a285efda46 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.
2026-10-17 13:49:49 +07:00

135 lines
6.1 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],
}
/// Submission callable from the desktop background worker.
pub type SharedPrintFn = std::sync::Arc<dyn Fn(&PrintJob<'_>) -> Result<String, String> + Send + Sync>;
/// 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>;
/// Desktop services can submit on the existing background worker; browser services stay
/// on the UI thread to invoke their print dialog.
fn shared_print(&self) -> Option<SharedPrintFn> {
None
}
/// 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, 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> {
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 ["preset", "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")?;
if let Some(submit) = service.shared_print() {
let pending_printer = printer.clone();
let mut pending = crate::background::run(
app,
true,
format!("Printing {title}"),
None,
move |_| {
submit(&PrintJob { printer: printer.as_deref(), title: &title, pdf: &pdf }).map(|note| json!({ "note": note, "printer": printer }))
},
move |app, result| {
let receipt = result?;
app.status(receipt.get("note").and_then(Value::as_str).unwrap_or_default());
Ok(json!({ "printer": receipt["printer"], "printed": true }))
},
)?;
if pending.get("background").and_then(Value::as_bool) == Some(true) {
// Acceptance by the worker is not acceptance by the spooler.
pending["printer"] = json!(pending_printer);
pending["printed"] = json!(false);
}
// Preserve the engine's pages, byte count, format and warnings in automation replies.
if let Some(fields) = pending.as_object() {
for (key, value) in fields {
v[key] = value.clone();
}
}
return Ok(v);
}
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)
}