Files
designcraft/crates/engine/src/cmd/print.rs
T

125 lines
5.6 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: the pages go to a printer through the system print spooler (craft-print), as PDF.
use serde_json::{Value, json};
#[cfg(not(target_arch = "wasm32"))]
use super::str_param;
use super::{CommandSpec, bad, cmd, has_doc};
#[cfg(not(target_arch = "wasm32"))]
use crate::EngineError;
use crate::{Result, Session};
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(query "file.printers", "Printers", [], None, "{} → {printers: [names], default}", super::always, |_, _| Ok(printers())),
cmd!(
noundo "file.print",
"Print…",
["File"],
Some("Cmd+P"),
"{printer?, copies?: 1, pages?: \"1-3,5\", spreads?, marks?, bleed?, dryRun?: bool (export and preview submission without printing), created?: int (Unix seconds, as file.exportPdf)} → {printer, copies, pages, command, bytes}",
has_doc,
print
),
]
}
fn printers() -> Value {
let printers = craft_print::printers();
let default = printers.iter().find(|p| p.default).map(|p| p.name.clone());
let names: Vec<_> = printers.into_iter().map(|p| p.name).collect();
json!({"printers": names, "default": default})
}
fn print(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(target_arch = "wasm32")]
{
super::export::options(p, s.doc()?.doc.page_count())?;
Err(bad("file.print", "printing isn't available on the web: export a PDF and print it"))
}
#[cfg(not(target_arch = "wasm32"))]
print_with(s, p, craft_print::submit)
}
#[cfg(not(target_arch = "wasm32"))]
fn print_with(
s: &mut Session,
p: &Value,
submit: impl FnOnce(&[u8], &craft_print::Job) -> std::result::Result<String, craft_print::Error>,
) -> Result<Value> {
let d = &s.doc()?.doc;
let opts = super::export::options(p, d.page_count())?;
let copies = p.get("copies").and_then(Value::as_u64).unwrap_or(1).clamp(1, 999) as u32;
let job = craft_print::Job {
printer: str_param(p, "printer").map(str::to_string),
copies,
scaling: craft_print::Scaling::ShrinkToFit,
title: d.title.clone(),
..craft_print::Job::default()
};
let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
let pdf = designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(d), &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let command = craft_print::command_preview(&job).map_err(|e| bad("file.print", e.to_string()))?;
let pages = opts.pages.as_ref().map_or(d.page_count(), |v| v.len());
let out = json!({"printer": job.printer, "copies": copies, "pages": pages, "command": command, "bytes": pdf.len()});
if !dry {
submit(&pdf, &job).map_err(|e| bad("file.print", e.to_string()))?;
}
Ok(out)
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn print_builds_the_spool_command() {
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 4})).unwrap();
let r = s.execute("file.print", &json!({"printer": "Office", "copies": 2, "pages": "2-3", "dryRun": true})).unwrap();
assert_eq!(r["pages"], 2);
let command: Vec<&str> = r["command"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
if craft_print::backend() == Some(craft_print::Backend::Cups) {
assert_eq!(&command[..5], ["lp", "-d", "Office", "-n", "2"]);
assert!(command.contains(&"print-scaling=auto-fit"));
assert!(!command.iter().any(|a| a.starts_with("sides=")));
} else {
assert_eq!(command[0], "powershell");
}
assert!(r["bytes"].as_u64().unwrap() > 500);
assert!(s.execute("file.print", &json!({"pages": "9", "dryRun": true})).is_err());
assert!(s.execute("file.printers", &json!({})).unwrap()["printers"].is_array());
}
#[test]
fn print_submits_the_selected_export_and_reports_refusal() {
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 4})).unwrap();
// A fixed creation date: both exports would otherwise take the clock's second.
let params = json!({"printer": "Office (east)", "copies": 2, "pages": "2-3", "marks": true, "bleed": true, "created": 1_700_000_000});
let expected = {
let d = &s.doc().unwrap().doc;
let options = super::super::export::options(&params, d.page_count()).unwrap();
designcraft_pdf::export_pdf(&super::super::interchange::with_pdf_boxes(d), &s.cache, &options).unwrap()
};
let r = print_with(&mut s, &params, |pdf, job| {
// The production export options and full bytes reach the shared spooler boundary.
assert_eq!(pdf, expected);
assert!(pdf.starts_with(b"%PDF-"));
assert_eq!(job.printer.as_deref(), Some("Office (east)"));
assert_eq!(job.copies, 2);
assert_eq!(job.duplex, None);
assert_eq!(job.scaling, craft_print::Scaling::ShrinkToFit);
assert!(!job.title.is_empty());
Ok("request id is Office-1".into())
})
.unwrap();
assert_eq!(r["pages"], 2);
let refusal = print_with(&mut s, &params, |_, _| Err(craft_print::Error::Spool("printer offline".into()))).unwrap_err();
assert!(refusal.to_string().contains("printer offline"));
print_with(&mut s, &json!({"dryRun": true}), |_, _| panic!("dry runs must not spool")).unwrap();
assert!(print_with(&mut s, &json!({"pages": "9"}), |_, _| panic!("invalid ranges must not spool")).is_err());
assert!(print_with(&mut s, &json!({"created": "today"}), |_, _| panic!("an invalid date must not spool")).is_err());
}
}