fix(print): adopt native Windows spooler

Adopt the canonical craft-print revision from craft-libs #28 so Windows printing uses the in-process Windows.Data.Pdf/XPS backend instead of PowerShell. Keep exact paper, orientation and scaling validation and the existing PDFCraft composition and permission checks.

Pin the reviewed winprint fork, scope its compile-time licence/advisory exceptions, and distinguish the earlier WDAC Sandbox exercise from the final pinned revision's Windows CI coverage. Physical printer output remains unverified.

Add worker-barrier regressions for Home-selected tool handoff and startup document ordering.

Fixes #1107

Co-authored-by: durtyrobbin <[email protected]>
Co-authored-by: Dimitar Klaturov <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: skylar taylor-barrick <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 14:46:24 -06:00
1 parent b5467d93a0
commit 3f044b6263
8 files changed
+1146 -76

No files matched your search

Generated
+1063 -67
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -15,7 +15,7 @@ repository = "https://github.com/storytold/pdfcraft"
# Shared frontend components; engines remain independent of egui.
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
# Shared system spooler; document composition remains in pdfcraft-print.
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "f03ec27ab2c405969428512178f746183cd55ca0" }
# internal
pdfcraft-geom = { path = "crates/geom", version = "0.7.0" }
pdfcraft-filters = { path = "crates/filters", version = "0.7.0" }
+1
View File
@@ -150,6 +150,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged.
- **2026-10-12 (M10, Windows printing without PowerShell, #1107):** craft-print now prints on Windows in-process: printers from EnumPrinters/GetDefaultPrinter, jobs drawn as vectors by Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling), so printing works where App Control for Business or AppLocker run PowerShell in Constrained Language Mode. Portrait and landscape sheets still print as one job (turned with /Rotate); a sheet size the driver lacks is refused rather than shrunk. Pins craft-libs f03ec27 (craft-libs#28); deny.toml allows the storytold/winprint.rs fork and adds a webpki-root-certs licence exception and a RUSTSEC-2024-0370 ignore for winprint's build-dependencies. An earlier craft-print revision was checked in Windows Sandbox under enforced WDAC: printers listed, CLI and desktop jobs spooled, and no powershell.exe in the 4688 audit. The final pinned revision passed craft-libs Windows CI, including a real paused-queue job; that Sandbox run has not been repeated. Estimates unchanged.
- **2026-10-12 (M13, CLI robustness scans):** `pdfcraft-cli check` visits a directory once even when input roots overlap or directory symlinks lead back into the tree. PDFs reached through file symlinks remain accepted. Three real CLI regressions cover overlapping roots, a directory cycle and directory/file aliases. Estimates unchanged.
- **2026-10-12 (M4, Type 3 extraction fidelity):** Extract and Split preserve shared image/resource dictionaries reached through Type 3 glyphs, including nested forms, patterns and soft masks. A bounded object-graph walk protects unknown glyph content; unreadable pages/forms retain their glyph resources, and shared explicit fonts are visited once. Separate page dictionaries still prune unused images. Synthetic save/reopen regressions and an opt-in Poppler pixel oracle cover resource ownership, nested forms, cycles, limits and repeated fonts. Estimates unchanged.
- **2026-10-12 (M2, TIFF predictor rows):** Packed RGB images using TIFF horizontal differencing keep every row when samples do not fill the last byte. The renderer aligns each decoded row before moving to the next. Synthetic decoding tests cover 1/2/4-bit samples, horizontal differencing and an 8-bit control; a red/green page-render regression verifies visible output. Estimates unchanged.
+4 -4
View File
@@ -71,7 +71,7 @@ this is not per-page paper-source selection.
**PDF scale** describes the source-to-sheet transform in the previewed print-ready
PDF, not measured physical output. Fit reserves an 18 pt margin and can reduce even
a matching page. The current Windows backend still rasterises and can substitute
paper/shrink a job if its driver lacks the requested size. These controls do not
add #599's strict PrintTicket validation or vector spooler. Verify the driver's
paper/scaling settings and physical calibration before relying on drawing scale.
a matching page. On Windows craft-print sends the sheets as vectors with a PrintTicket
for the sheet's exact paper and no scaling (#599's approach), validated by the driver:
a job is refused, not shrunk, when the driver lacks the paper or would change it. Verify
the driver's paper settings and physical calibration before relying on drawing scale.
+60
View File
@@ -346,3 +346,63 @@ fn startup_intake_waits_through_wrong_password_retry_and_cancel_preserves_the_ex
assert_eq!(app.session.docs().iter().find(|doc| doc.name == "startup.pdf").expect("the startup document is available").info.pages.len(), 1);
assert_eq!(app.session.get(existing).expect("the existing document remains open").bytes, original);
}
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
fn assert_tool_runs_after_background_open(extension: &str, bytes: Vec<u8>, pages: usize) {
let (mut harness, _client) = harness();
harness.state_mut().close_tab(0);
let (gate, started, release, finished) = open_worker::prepare_gate();
harness.state_mut().open_prepare_gate = Some(gate);
let path = std::env::temp_dir().join(format!("pdfcraft-tool-worker-{}.{extension}", std::process::id()));
std::fs::write(&path, &bytes).expect("write picked document");
harness.state_mut().pick_override = Some(vec![path.to_string_lossy().into_owned()]);
harness.state_mut().run_command("export.text");
harness.step();
started.recv_timeout(Duration::from_secs(5)).expect("the picked document reached preparation");
assert!(harness.state().active.is_none());
assert!(harness.state().dialog.is_none(), "the tool waits for its document");
release.send(()).expect("release picked document preparation");
finished.recv_timeout(Duration::from_secs(5)).expect("picked document prepared");
harness.step();
let app = harness.state();
assert_eq!(app.views.len(), 1, "the chosen document opens once");
assert_eq!(app.session.get(app.views[0].id).expect("chosen document").info.pages.len(), pages);
assert!(matches!(app.dialog, Some(crate::Dialog::Export(_))), "the chosen tool runs after preparation: {:?}", app.dialog);
harness.run_steps(2);
assert_eq!(harness.state().views.len(), 1);
assert_eq!(std::fs::read(&path).expect("read unchanged input"), bytes);
let _ = std::fs::remove_file(path);
}
#[test]
fn tool_from_home_runs_after_pdf_preparation() {
assert_tool_runs_after_background_open("pdf", fixture(2), 2);
}
#[test]
fn tool_from_home_runs_after_text_conversion() {
assert_tool_runs_after_background_open("txt", b"Synthetic document for a chosen tool".to_vec(), 1);
}
#[test]
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
fn a_startup_document_waits_for_an_active_document_to_prepare() {
let (mut harness, _client) = harness();
harness.state_mut().close_tab(0);
let (gate, started, release, finished) = open_worker::prepare_gate();
harness.state_mut().open_prepare_gate = Some(gate);
let path = std::env::temp_dir().join(format!("pdfcraft-startup-worker-{}.pdf", std::process::id()));
std::fs::write(&path, fixture(2)).expect("write chosen document");
harness.state_mut().open_path(&path.to_string_lossy());
started.recv_timeout(Duration::from_secs(5)).expect("chosen document reached preparation");
harness.state().startup_inbox.lock().expect("startup inbox").push(("startup.pdf".into(), fixture(1)));
harness.step();
assert_eq!(harness.state().startup_inbox.lock().expect("startup inbox").len(), 1, "startup bytes wait for the pending active open");
assert!(harness.state().views.is_empty());
release.send(()).expect("release chosen document preparation");
finished.recv_timeout(Duration::from_secs(5)).expect("chosen document prepared");
harness.step();
assert_eq!(harness.state().views.len(), 2);
assert_eq!(harness.state().active, Some(0), "the chosen document keeps focus");
let _ = std::fs::remove_file(path);
}
+1 -1
View File
@@ -178,7 +178,7 @@ impl PrintDraft {
}
/// Lists the printers off the UI thread (the desktop app sets [`PdfCraftApp::printer_source`]):
/// on Windows that takes PowerShell a second or more, and CUPS' driverless discovery can wait on
/// on Windows each driver's capabilities are read for its two-sided support, and CUPS' driverless discovery can wait on
/// the network. Without a source the dialog reads them as it opens, so tests stay deterministic.
pub type PrinterSource = std::sync::Arc<dyn Fn() -> Vec<spool::Printer> + Send + Sync>;
+15 -2
View File
@@ -32,8 +32,9 @@ allow-wildcard-paths = true
[sources]
unknown-registry = "deny"
unknown-git = "deny"
# Shared UI components; Cargo.toml pins the immutable revision.
allow-git = ["https://github.com/storytold/craft-libs"]
# Shared UI components and spooler; Cargo.toml pins the immutable revision. craft-print pins
# storytold's winprint fork (Windows printing, #1107) to an immutable revision of its own.
allow-git = ["https://github.com/storytold/craft-libs", "https://github.com/storytold/winprint.rs"]
[advisories]
yanked = "warn"
@@ -43,6 +44,10 @@ ignore = [
# private-key operations use aws-lc-rs on native targets and are refused in the browser
# (ADR-0009). Revisit when a fixed `rsa` ships.
{ id = "RUSTSEC-2023-0071", reason = "rsa is used for verification only (ADR-0009)" },
# proc-macro-error is unmaintained (no vulnerability). It reaches the tree as a proc macro of
# winprint's fmt-derive (craft-print's Windows spooler), so it runs at compile time on Windows
# and is not in the binary.
{ id = "RUSTSEC-2024-0370", reason = "compile-time proc macro of winprint's fmt-derive (craft-print Windows printing, #1107)" },
]
# egui's default UI fonts (Ubuntu-Light, Hack, emoji-icon-font): font licences on the AGENTS.md
@@ -50,3 +55,11 @@ ignore = [
[[licenses.exceptions]]
crate = "epaint_default_fonts"
allow = ["OFL-1.1", "Ubuntu-font-1.0"]
# winprint (craft-print's Windows spooler, #1107) lists reqwest as a build-dependency for its
# PDFium download, which only runs with its `pdfium` feature; craft-print turns that off. The CA
# bundle reqwest brings is CDLA-Permissive-2.0 data and is compiled into winprint's build script
# only, never into PdfCraft. Remove when winprint makes those build-dependencies optional.
[[licenses.exceptions]]
crate = "webpki-root-certs"
allow = ["CDLA-Permissive-2.0"]
+1 -1
View File
@@ -7925,7 +7925,7 @@ status = "shipped"
commands = ["print.dialog"]
tools = ["printers", "printer_options", "doc_print"]
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::windows_printers_carry_one_default_and_keep_their_names", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options", "crates/ui-egui/tests/print.rs::printers_listed_in_the_background_preselect_the_default_unless_one_was_picked", "crates/ui-egui/src/web_print.rs::resolution_falls_with_the_sheet_count_and_long_jobs_are_refused", "crates/ui-egui/src/web_print.rs::large_sheets_are_drawn_within_the_pixel_cap_and_bad_sizes_are_refused", "crates/ui-egui/src/web_print.rs::a_sink_error_or_a_page_that_isnt_there_stops_the_job"]
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows the queues and the default come from Win32_Printer (PowerShell CIM) and jobs are rendered with Windows.Data.Pdf and spooled through shared craft-print at each sheet's physical size and orientation, with bounded raster resolution up to 600 dpi (not vector passthrough); the printer list and submission run on workers without a console window. Native Windows regression execution is pending; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job. On the web: \"Browser's print dialog\" (the default there) reaches the system's printers through the browser; PdfCraft draws the sheets itself (300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000; at most 64 megapixels a sheet and 1 GiB a job) and prints them from a hidden iframe, one per printed page, a landscape sheet among portrait ones turned onto the paper; copies, two-sided and colour are chosen in the browser's dialog; a browser that refuses to print gets the print-ready PDF as a download. Checked in headless Chromium, not yet in Firefox or Safari; pictures, not vectors; sheets of another paper size print at their own size only where the browser honours named @page sizes (Chromium doesn't)"
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows shared craft-print lists the queues (EnumPrinters), the default (GetDefaultPrinter) and each driver's two-sided support in this process, and draws jobs as vectors with Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling; portrait and landscape sheets in one job via /Rotate), starting no PowerShell, so printing works under App Control script enforcement (#1107); the printer list and submission run on workers; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job. On the web: \"Browser's print dialog\" (the default there) reaches the system's printers through the browser; PdfCraft draws the sheets itself (300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000; at most 64 megapixels a sheet and 1 GiB a job) and prints them from a hidden iframe, one per printed page, a landscape sheet among portrait ones turned onto the paper; copies, two-sided and colour are chosen in the browser's dialog; a browser that refuses to print gets the print-ready PDF as a download. Checked in headless Chromium, not yet in Firefox or Safari; pictures, not vectors; sheets of another paper size print at their own size only where the browser honours named @page sizes (Chromium doesn't)"
[[feature]]
id = "print.copies-collate"