Skip to main content

export_engine/
pdf.rs

1// PDF via wkhtmltopdf/weasyprint/Chromium. Logic moved from peek-cli `run_export_pdf`.
2
3use anyhow::Result;
4
5use crate::html::render_html;
6use crate::snapshot::ProcessSnapshot;
7
8/// Render a PDF report for the given snapshot and return the output filename.
9///
10/// The implementation searches for a supported renderer (wkhtmltopdf,
11/// weasyprint, Chromium/Chrome). It writes an HTML file to the system temp
12/// directory, invokes the renderer, then removes the temp HTML file.
13pub fn export_pdf(snapshot: &ProcessSnapshot) -> Result<String> {
14    let html = render_html(snapshot);
15    let info = &snapshot.process;
16
17    // Find a PDF renderer (portable: use which crate instead of shell "which")
18    let renderer = ["wkhtmltopdf", "weasyprint", "chromium", "google-chrome"]
19        .iter()
20        .find(|cmd| which::which(cmd).is_ok())
21        .copied();
22
23    let Some(renderer) = renderer else {
24        anyhow::bail!(
25            "PDF export requires wkhtmltopdf, weasyprint, or Chromium. \
26             Install one and try again, or use --export html."
27        );
28    };
29
30    let filename = format!("peek-{}-{}.pdf", info.name, info.pid);
31    let out_path = std::env::current_dir()
32        .unwrap_or_else(|_| std::path::PathBuf::from("."))
33        .join(&filename);
34    let out_path_str = out_path
35        .to_str()
36        .ok_or_else(|| anyhow::anyhow!("output path is not valid UTF-8"))?
37        .to_string();
38
39    // Write HTML to a temp file
40    let tmp = std::env::temp_dir().join(format!("peek-{}.html", info.pid));
41    std::fs::write(&tmp, &html)?;
42    let tmp_str = tmp.to_string_lossy();
43
44    let status = match renderer {
45        "wkhtmltopdf" => std::process::Command::new("wkhtmltopdf")
46            .args([tmp_str.as_ref(), out_path_str.as_str()])
47            .status()?,
48        "weasyprint" => std::process::Command::new("weasyprint")
49            .args([tmp_str.as_ref(), out_path_str.as_str()])
50            .status()?,
51        _ => std::process::Command::new(renderer)
52            .args([
53                "--headless",
54                "--disable-gpu",
55                "--print-to-pdf",
56                &out_path_str,
57                &format!("file://{}", tmp.display()),
58            ])
59            .status()?,
60    };
61
62    let _ = std::fs::remove_file(&tmp);
63
64    if !status.success() {
65        anyhow::bail!("{} exited with status {:?}", renderer, status.code());
66    }
67
68    Ok(out_path_str)
69}