diff --git a/Cargo.lock b/Cargo.lock index 138ca3d814..99799dc20f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4810,6 +4810,7 @@ dependencies = [ "tempfile", "thiserror 2.0.21", "wat", + "winsafe", ] [[package]] diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 741d927a3f..e881dca4cf 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -43,6 +43,11 @@ roxmltree = "0.20" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tempfile = "3" +# `file.print` loads Windows.Data.Pdf.dll before calling into it (#5884): the safe binding +# ui-egui and craft-print already use, pinned to the same version. +[target.'cfg(target_os = "windows")'.dependencies] +winsafe = { version = "=0.0.29", default-features = false, features = ["kernel"] } + [dev-dependencies] photocraft-affinity = { workspace = true, features = ["synth"] } # Synthetic camera raw files for the open tests. diff --git a/crates/engine/src/print_cmds.rs b/crates/engine/src/print_cmds.rs index 88ed6c7883..72af145782 100644 --- a/crates/engine/src/print_cmds.rs +++ b/crates/engine/src/print_cmds.rs @@ -437,24 +437,31 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result { } /// Windows prints a PDF through Windows.Data.Pdf.dll, which the apps delay-load (see their -/// `build.rs`) so a Windows image without it still starts. Calling into it when it is missing -/// would abort the process, so check that it is installed first (#5884). +/// `build.rs`) so a Windows image without it still starts. Calling into it when it cannot load +/// would abort the process, so check that it loads first (#5884). fn require_windows_pdf() -> Result<()> { - if !cfg!(windows) { - return Ok(()); - } - let root = std::env::var_os("SystemRoot").unwrap_or_else(|| std::ffi::OsString::from(r"C:\Windows")); - if windows_pdf_installed(std::path::Path::new(&root)) { - Ok(()) - } else { - Err(other( - "printing this page needs the Windows PDF component (Windows.Data.Pdf.dll), which is not installed. Save as PDF and print it from another app, or clear Print Settings to print directly.", - )) + #[cfg(target_os = "windows")] + { + let root = std::env::var_os("SystemRoot").unwrap_or_else(|| std::ffi::OsString::from(r"C:\Windows")); + if !windows_pdf_loads(&windows_pdf_dll(std::path::Path::new(&root))) { + return Err(other( + "printing this page needs the Windows PDF component (Windows.Data.Pdf.dll), which is missing or cannot load. Save as PDF and print it from another app, or print an RGB image without page text or labels to print directly.", + )); + } } + Ok(()) } -fn windows_pdf_installed(system_root: &std::path::Path) -> bool { - system_root.join("System32").join("Windows.Data.Pdf.dll").is_file() +#[cfg(target_os = "windows")] +fn windows_pdf_dll(system_root: &std::path::Path) -> std::path::PathBuf { + system_root.join("System32").join("Windows.Data.Pdf.dll") +} + +/// Loads the DLL rather than checking the file exists: one that is present but cannot load (a +/// dependency stripped too) would still abort at the delay-load stub. +#[cfg(target_os = "windows")] +fn windows_pdf_loads(dll: &std::path::Path) -> bool { + dll.to_str().is_some_and(|p| winsafe::HINSTANCE::LoadLibrary(p).is_ok()) } /// Keep CMYK output profiles and page text on the existing PDF composition path. GDI currently diff --git a/crates/engine/src/print_cmds/tests.rs b/crates/engine/src/print_cmds/tests.rs index 677fdec6ba..431b83343a 100644 --- a/crates/engine/src/print_cmds/tests.rs +++ b/crates/engine/src/print_cmds/tests.rs @@ -435,12 +435,28 @@ fn printers_lists_the_spooler_destinations_without_a_document() { } } +#[cfg(target_os = "windows")] #[test] -fn windows_pdf_component_is_detected_by_its_dll() { - // #5884: a Windows image without Windows.Data.Pdf.dll reports an error instead of aborting. +fn windows_pdf_component_is_detected_by_loading_its_dll() { + // #5884: a Windows image without a loadable Windows.Data.Pdf.dll reports an error instead of + // aborting at the delay-load stub. let dir = tempfile::tempdir().unwrap(); - assert!(!windows_pdf_installed(dir.path())); + let dll = windows_pdf_dll(dir.path()); + assert!(!windows_pdf_loads(&dll)); + // Present but not loadable counts as missing. std::fs::create_dir(dir.path().join("System32")).unwrap(); - std::fs::write(dir.path().join("System32").join("Windows.Data.Pdf.dll"), b"MZ").unwrap(); - assert!(windows_pdf_installed(dir.path())); + std::fs::write(&dll, b"MZ").unwrap(); + assert!(!windows_pdf_loads(&dll)); + let root = std::env::var_os("SystemRoot").unwrap(); + let system = windows_pdf_dll(std::path::Path::new(&root)); + if system.is_file() { + assert!(windows_pdf_loads(&system)); + assert!(require_windows_pdf().is_ok()); + } +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn the_windows_pdf_check_is_a_no_op_elsewhere() { + assert!(require_windows_pdf().is_ok()); }