mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
* 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>
71 lines
3.5 KiB
Rust
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()))
|
|
}
|