Files
pdfcraft/crates/print/src/spool.rs
T
9b6dab142f M10: Windows print jobs show per-sheet progress; printers list faster (follow-up to #944) (#1236)
* M10: Windows print jobs show per-sheet progress; printers list faster

Follow-up to #944, carrying the two parts main doesn't have yet.

- The progress card follows a job being spooled and counts its sheets
  as they reach the spooler ("Sending sheet 2 of 3 to Office…"), using
  craft-print's new submit_with_progress. It replaces the "Sending to…"
  toast, leaves another long job's card alone, and goes when the job
  ends; the spooler's answer is still a notice.
- craft-print bumped to storytold/craft-libs#27, which also lists
  Windows printers through .NET's PrinterSettings (the API jobs spool
  with) instead of a CIM query of Win32_Printer.
- "Sending sheet {s} of {n} to {printer}…" in all 20 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refresh catalog hashes after merging main

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 03:04:29 -04:00

71 lines
3.5 KiB
Rust

//! System print spooler supplied by craft-print. PDF composition and permission checks
//! stay in PdfCraft; these adapters retain the application's print-error surface.
pub use craft_print::{
Duplex, HANDLED_OPTIONS, HAS_PRINTER_PREFERENCES, Job, Printer, PrinterOption, Scaling, lp_args, lpstat_command, parse_lpoptions, parse_lpstat,
parse_lpstat_e, parse_ppd, parse_windows_printers, ppd_text, printers, printers_parsed,
};
/// The driver's job options for a printer (CUPS: its PPD, with the queue's current defaults);
/// empty when it has none or on other platforms. Reads files and runs `lpoptions`, so call it
/// when the user asks, not every frame.
pub fn printer_options(printer: &str) -> Vec<PrinterOption> {
#[cfg(all(unix, not(target_arch = "wasm32")))]
{
printer_options_via(printer, std::path::Path::new("/etc/cups/ppd"), std::process::Command::new("lpoptions"))
}
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
{
craft_print::printer_options(printer)
}
}
/// Read a destination's options with the PPD directory and command supplied, so tests need
/// neither a system queue nor a physical printer. CUPS instances share their queue's PPD but
/// have their own defaults, queried with the complete `queue/instance` destination.
#[cfg(all(unix, not(target_arch = "wasm32")))]
pub(crate) fn printer_options_via(printer: &str, ppd_dir: &std::path::Path, mut lpoptions: std::process::Command) -> Vec<PrinterOption> {
let (queue, instance) = printer.split_once('/').map_or((printer, None), |(q, i)| (q, Some(i)));
// Retain the conservative name checks, applied to each component. Neither component is
// a path: reject dot components, backslashes and any additional separator before I/O.
let valid = |name: &str| {
!name.is_empty()
&& name.len() <= 64
&& !matches!(name, "." | "..")
&& name.bytes().all(|b| b.is_ascii_graphic() && !matches!(b, b'/' | b'\\' | b':' | b'=' | b'"'))
};
if !valid(queue) || instance.is_some_and(|name| !valid(name)) {
return Vec::new();
}
const PPD_CAP: u64 = 16 << 20;
let ppd = std::fs::File::open(ppd_dir.join(format!("{queue}.ppd"))).ok().and_then(|f| {
use std::io::Read as _;
let mut bytes = Vec::new();
f.take(PPD_CAP).read_to_end(&mut bytes).ok().map(|_| bytes)
});
let mut options = ppd.map(|b| parse_ppd(&ppd_text(&b))).unwrap_or_default();
let current = lpoptions.args(["-p", printer, "-l"]).output().map(|o| parse_lpoptions(&String::from_utf8_lossy(&o.stdout))).unwrap_or_default();
for (key, value) in current {
if let Some(o) = options.iter_mut().find(|o| o.key == key)
&& o.choices.iter().any(|(c, _)| *c == value)
{
o.default = value;
}
}
options
}
pub fn submit(pdf: &[u8], job: &Job) -> Result<String, crate::PrintError> {
craft_print::submit(pdf, job).map_err(|e| crate::PrintError::Spool(e.to_string()))
}
/// [`submit`], told `(sent, sheets)` as each sheet reaches the spooler (Windows; CUPS takes the
/// whole PDF at once and reports nothing).
pub fn submit_with_progress(pdf: &[u8], job: &Job, progress: &mut dyn FnMut(usize, usize)) -> Result<String, crate::PrintError> {
craft_print::submit_with_progress(pdf, job, progress).map_err(|e| crate::PrintError::Spool(e.to_string()))
}
pub fn open_printer_preferences(printer: &str) -> Result<(), crate::PrintError> {
craft_print::open_printer_preferences(printer).map_err(|e| crate::PrintError::Spool(e.to_string()))
}