mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 21:11:52 +00:00
fix(print): guard paper discovery and retain explicit custom stock
This commit is contained in:
1 parent
de24a4ddba
commit
257c026c8a
6 files changed
+123
-6
No files matched your search
@@ -75,9 +75,9 @@ fn resolve_paper(name: Option<&str>, printer: Option<&str>) -> Result<((f64, f64
|
||||
if let Some(size) = parse_custom_dimensions(p) {
|
||||
return Ok((size, Some(print::paper_media(None, size))));
|
||||
}
|
||||
// 4. A size of its own: "8.5x14in", "210x297mm", "10x15cm". It names no media.
|
||||
// 4. Explicit units request the same stock as point dimensions.
|
||||
if let Some(size) = paper_size(p) {
|
||||
return Ok((size, None));
|
||||
return Ok((size, Some(print::paper_media(None, size))));
|
||||
}
|
||||
return Err(bad(format!(
|
||||
"unknown paper {p:?}; choose one of {}, a paper from printer_papers, or a size such as 8.5x14in or 210x297mm (1 to 200 inches a side)",
|
||||
@@ -450,9 +450,19 @@ mod tests {
|
||||
assert_eq!(job(json!({}), Some("Custom.300x500pt")).options, [media("Custom.300x500")]);
|
||||
|
||||
assert!(matches!(resolve_paper(Some("Quarto"), None), Err(ToolError::InvalidArgs(m)) if m.contains("US Letter")));
|
||||
// A size of its own is laid out as asked and names no media.
|
||||
assert_eq!(resolve_paper(Some("8.5x14in"), None).ok(), Some(((612.0, 1008.0), None)));
|
||||
assert!(job(json!({}), Some("8.5x14in")).options.is_empty());
|
||||
// Every accepted dimension syntax requests the same stock from the queue.
|
||||
for (input, size) in [
|
||||
("8.5x14in", (612.0, 1008.0)),
|
||||
("210x297mm", (210.0 * 72.0 / 25.4, 297.0 * 72.0 / 25.4)),
|
||||
("10x15cm", (10.0 * 72.0 / 2.54, 15.0 * 72.0 / 2.54)),
|
||||
("300x500pt", (300.0, 500.0)),
|
||||
] {
|
||||
let expected = print::paper_media(None, size);
|
||||
let (resolved_size, resolved_media) = resolve_paper(Some(input), None).unwrap();
|
||||
assert!((resolved_size.0 - size.0).abs() < 0.01 && (resolved_size.1 - size.1).abs() < 0.01, "{input}");
|
||||
assert_eq!(resolved_media.as_deref(), Some(expected.as_str()), "{input}");
|
||||
assert_eq!(job(json!({}), Some(input)).options, [media(&expected)], "{input}");
|
||||
}
|
||||
assert_eq!(spool_job(&Args(&json!({})), "default", String::new(), None, Some("Letter"), None, &[]).ok().map(|j| j.printer), Some(None));
|
||||
assert!(spool_job(&Args(&json!({ "options": { "InputSlot": 3 } })), "Office", String::new(), None, Some("Letter"), None, &[]).is_err());
|
||||
}
|
||||
|
||||
@@ -460,12 +460,18 @@ pub type Paper = (String, (f64, f64));
|
||||
/// Empty when the printer reports none or on platforms where driver capabilities are not queried.
|
||||
#[cfg(windows)]
|
||||
pub fn printer_papers(printer: &str) -> Vec<Paper> {
|
||||
// A call into a missing delay-loaded PrintTicket DLL would terminate the process (#2630).
|
||||
if missing_print_dll().is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
let _provider = print_ticket_provider();
|
||||
printer_papers_internal(printer)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn printer_papers_internal(printer: &str) -> Vec<Paper> {
|
||||
#[cfg(test)]
|
||||
PRINTER_BACKEND_CALLS.with(|c| c.set(c.get().saturating_add(1)));
|
||||
use winprint::ticket::{FeatureOptionPack as _, FeatureOptionPackWithPredefined as _};
|
||||
let Ok(devices) = winprint::printer::PrinterDevice::all() else { return Vec::new() };
|
||||
let Some(device) = devices.into_iter().find(|d| d.name() == printer) else { return Vec::new() };
|
||||
@@ -517,6 +523,9 @@ pub fn printer_default_paper(printer: &str) -> Option<Paper> {
|
||||
|
||||
/// Reads the supported paper sizes and the default paper size configured for `printer`.
|
||||
pub fn printer_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
|
||||
if missing_print_dll().is_some() {
|
||||
return (Vec::new(), None);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
let _provider = print_ticket_provider();
|
||||
#[cfg(windows)]
|
||||
@@ -543,6 +552,9 @@ pub fn printer_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
|
||||
/// settings (such as tray or quality) are not captured; general driver settings should
|
||||
/// be configured through [`open_printer_preferences`] (Properties…).
|
||||
pub fn show_printer_properties(printer: &str, parent: Option<isize>) -> Result<Option<Paper>, crate::PrintError> {
|
||||
if let Some(dll) = missing_print_dll() {
|
||||
return Err(unavailable(dll));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let devmode = match craft_print::rgb::settings_dialog(printer, None, parent) {
|
||||
|
||||
@@ -1690,6 +1690,36 @@ fn printing_without_the_print_dlls_is_refused_before_the_spooler() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #2630 and #6100: paper discovery must respect the same delay-load guard as submission.
|
||||
/// The nonexistent queue makes a missing guard observable without requiring printer hardware.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn paper_detection_without_print_dlls_never_reaches_the_driver() {
|
||||
use crate::spool::{PRINT_DLLS, PRINTER_BACKEND_CALLS, SYSTEM32_FOR_TEST};
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-paper-no-dlls-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for missing in PRINT_DLLS {
|
||||
SYSTEM32_FOR_TEST.with(|d| *d.borrow_mut() = Some(dir.clone()));
|
||||
PRINTER_BACKEND_CALLS.with(|c| c.set(0));
|
||||
let name = "PdfCraft test: no such printer";
|
||||
let papers = crate::spool::printer_papers(name);
|
||||
let info = crate::spool::printer_paper_info(name);
|
||||
let default = crate::spool::printer_default_paper(name);
|
||||
let calls = PRINTER_BACKEND_CALLS.with(|c| c.get());
|
||||
// A broken guard must not open a real dialog while this regression fails.
|
||||
let properties = (calls == 0).then(|| crate::spool::show_printer_properties(name, None));
|
||||
SYSTEM32_FOR_TEST.with(|d| *d.borrow_mut() = None);
|
||||
assert!(papers.is_empty());
|
||||
assert_eq!(info, (Vec::new(), None));
|
||||
assert_eq!(default, None);
|
||||
assert_eq!(calls, 0, "paper queries must not reach the backend without {missing}");
|
||||
assert!(matches!(properties, Some(Err(crate::PrintError::Spool(m))) if m.contains(missing)));
|
||||
std::fs::write(dir.join(missing), b"").unwrap();
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printer_papers_and_default_paper_api() {
|
||||
// Nonexistent printer returns empty
|
||||
|
||||
@@ -107,6 +107,8 @@ impl PrintDraft {
|
||||
self.custom_papers.remove(0);
|
||||
}
|
||||
self.custom_papers.push(paper);
|
||||
// Add is an explicit choice too: an outstanding driver read must not replace it.
|
||||
self.paper_overridden = true;
|
||||
self.paper = self.papers.len() + self.custom_papers.len() - 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -540,7 +540,8 @@ impl PrintDraft {
|
||||
}
|
||||
.into_iter()
|
||||
// The sheet the PDF was laid out on, not the queue's default paper.
|
||||
.chain(Some(print::paper_media_option(Some(&self.paper_name()), self.paper_size())))
|
||||
// A user's custom label is not a keyword from the driver's media catalog.
|
||||
.chain(Some(print::paper_media_option((!self.paper_is_custom()).then(|| self.paper_name()).as_deref(), self.paper_size())))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,6 +1538,68 @@ fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
h.run_steps(8);
|
||||
}
|
||||
|
||||
/// A paper explicitly added while discovery is pending stays selected after its reply and reopen.
|
||||
#[test]
|
||||
fn adding_custom_paper_overrides_pending_driver_default() {
|
||||
use pdfcraft_engine::print::spool::Printer;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
let mut h = harness();
|
||||
let (release_tx, release_rx) = mpsc::channel();
|
||||
let (started_tx, started_rx) = mpsc::channel();
|
||||
let first_read = Arc::new(Mutex::new(Some(release_rx)));
|
||||
h.state_mut().printer_source =
|
||||
Some(Arc::new(|| vec![Printer { name: "Labeller".into(), default: true, two_sided: None, description: String::new() }]));
|
||||
h.state_mut().printer_papers_source = Some(Arc::new(move |_| {
|
||||
let pending = first_read.lock().unwrap().take();
|
||||
if let Some(rx) = pending {
|
||||
let _ = started_tx.send(());
|
||||
let _ = rx.recv();
|
||||
}
|
||||
vec![("Driver label".into(), (288.0, 432.0))]
|
||||
}));
|
||||
h.state_mut().default_paper_source = Some(Arc::new(|_| Some(("Driver label".into(), (288.0, 432.0)))));
|
||||
assert!(h.state_mut().execute("print.dialog"));
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
loop {
|
||||
h.run_steps(1);
|
||||
if started_rx.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
assert!(std::time::Instant::now() < deadline, "paper discovery started");
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
h.get_by_label("Custom…").click();
|
||||
h.run_steps(2);
|
||||
let units = h.state().print_draft.units.per_point();
|
||||
h.state_mut().print_draft.custom_new = Some(("Card".into(), format!("{}", 300.0 * units), format!("{}", 500.0 * units)));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Add").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().print_draft.paper_overridden, "Add is an explicit paper choice");
|
||||
assert!(!h.state().print_draft.papers_loading(), "the explicit choice need not wait for detection");
|
||||
release_tx.send(()).unwrap();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while h.state().print_draft.papers_loading.is_some() {
|
||||
assert!(std::time::Instant::now() < deadline, "paper discovery completed");
|
||||
h.run_steps(1);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert_eq!(h.state().print_draft.paper_name(), "Card");
|
||||
let (w, h_pts) = h.state().print_draft.paper_size();
|
||||
assert!((w - 300.0).abs() < 0.01 && (h_pts - 500.0).abs() < 0.01);
|
||||
h.state_mut().open_print();
|
||||
settle(&mut h);
|
||||
assert_eq!(h.state().print_draft.paper_name(), "Card", "reopening keeps the explicit choice");
|
||||
}
|
||||
|
||||
/// A name entered by the user is not a keyword from the printer's media catalog.
|
||||
#[test]
|
||||
fn a_named_custom_paper_requests_its_dimensions_from_the_spooler() {
|
||||
let mut d = pdfcraft_ui_egui::PrintDraft::default();
|
||||
d.add_custom_paper(serde_json::from_value(serde_json::json!({"name": "Card", "width": 300.0, "height": 500.0})).unwrap());
|
||||
assert_eq!(d.job("card.pdf").options, [("media".to_string(), "Custom.300x500".to_string())]);
|
||||
}
|
||||
|
||||
/// Page setup: a paper size of one's own joins the Paper list (Custom…) and prints at its size.
|
||||
/// The printer it was printed on keeps it (#2001) by name, across a restart, and Remove takes it
|
||||
/// off the list; the printer's setup then leaves the paper as it is.
|
||||
|
||||
Reference in new issue
Block a user