From 66edead348003a9368a4e3dbde5a42311858be1a Mon Sep 17 00:00:00 2001 From: pechnikov2026-star Date: Sun, 11 Oct 2026 20:16:12 +0300 Subject: [PATCH] demo-pdf: two fixes so xtask demo-pdf and xtask screenshots work on Windows (#954) * demo-pdf: build a working file:// URL on Windows Windows Path::canonicalize returns a verbatim \?\C:\... path. After the backslash swap that is //?/C:/..., which the Unix/Windows branch then misreads as an absolute Unix path, producing file:////?/C:/... Chrome fails to load that URL and prints one blank page, so on Windows cargo xtask demo-pdf and cargo xtask screenshots always fail with "Chrome produced 1 pages but the showcase layout expects 12". Strip the //?/ prefix before classifying, and add a regression test. * demo-pdf: don't accept Skia's incremental %%EOF writes as a finished PDF Chrome writes the print-to-pdf output incrementally and every increment ends with its own %%EOF, so the completion poll accepted the first ~29 KB increment (one page of twelve) as final and killed Chrome mid-print. Require the file size to stay quiet for 10 consecutive 500 ms polls before treating the file as finished; waiting for process exit alone is not enough because some Chrome builds linger after writing. --------- Co-authored-by: pechnikov2026-star --- xtask/src/demo_pdf/chrome.rs | 17 +++++++++++++---- xtask/src/demo_pdf/mod.rs | 23 ++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/xtask/src/demo_pdf/chrome.rs b/xtask/src/demo_pdf/chrome.rs index f1a86da17..dda608a6d 100644 --- a/xtask/src/demo_pdf/chrome.rs +++ b/xtask/src/demo_pdf/chrome.rs @@ -94,6 +94,10 @@ pub fn print_to_pdf(chrome: &Path, html: &Path, pdf: &Path, log: &Path) -> Resul let started = Instant::now(); let mut last_len = None; + // Chrome's Skia back-end writes the PDF incrementally, and every increment ends with its own + // `%%EOF`: a stable file that looks complete can still grow while later pages rasterise. Only + // treat the file as finished after the size has been quiet for several consecutive polls. + let mut quiet_polls = 0; loop { if let Some(status) = child.try_wait()? { if is_complete(pdf) { @@ -102,10 +106,15 @@ pub fn print_to_pdf(chrome: &Path, html: &Path, pdf: &Path, log: &Path) -> Resul bail!("Chrome exited with {status} without writing {}; see {}", pdf.display(), log.display()); } let len = fs::metadata(pdf).map(|m| m.len()).ok(); - if len.is_some() && len == last_len && is_complete(pdf) { - let _ = child.kill(); - let _ = child.wait(); - return Ok(()); + if len == last_len && len.is_some() && is_complete(pdf) { + quiet_polls += 1; + if quiet_polls >= 10 { + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + } else { + quiet_polls = 0; } last_len = len; if started.elapsed() > Duration::from_secs(120) { diff --git a/xtask/src/demo_pdf/mod.rs b/xtask/src/demo_pdf/mod.rs index 9a21690a7..a1bb84db7 100644 --- a/xtask/src/demo_pdf/mod.rs +++ b/xtask/src/demo_pdf/mod.rs @@ -97,7 +97,13 @@ fn repo_root() -> PathBuf { /// A `file://` URL for an absolute path, percent-encoding unsafe bytes. pub(crate) fn file_url(path: &Path) -> String { let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); - let s = abs.to_string_lossy().replace('\\', "/"); + let mut s = abs.to_string_lossy().replace('\\', "/"); + // Windows `canonicalize` returns a verbatim `\\?\C:\...` path; after the slash swap that is + // `//?/C:/...`, which must not be mistaken for an absolute Unix path, or the URL Chrome is + // handed becomes `file:////?/C:/...` and it prints one blank page instead of the document. + if let Some(rest) = s.strip_prefix("//?/") { + s = rest.to_string(); + } let mut url = String::from(if s.starts_with('/') { "file://" } else { "file:///" }); for byte in s.bytes() { match byte { @@ -185,3 +191,18 @@ fn foreign_fonts(doc: &lopdf::Document) -> Vec { } bad.into_iter().collect() } + +#[cfg(test)] +mod tests { + use super::*; + + /// On Windows `canonicalize` returns a verbatim `\?\C:\...` path; the URL must not carry the + /// `//?/` prefix over (regression test for `file:////?/C:/...`, which Chrome prints as one + /// blank page). + #[test] + fn file_url_has_no_verbatim_prefix() { + let url = file_url(&std::env::temp_dir()); + assert!(!url.contains("//?/"), "{url}"); + assert!(url.starts_with("file:///"), "{url}"); + } +}