nu_analytics/core/report/formats/
pdf.rs

1//! PDF report generator via HTML-to-PDF conversion
2//!
3//! Generates PDF reports by first creating an HTML report and then converting
4//! it to PDF using headless Chrome/Chromium or another specified converter.
5//!
6//! This approach provides:
7//! - High-quality PDFs with proper graph rendering
8//! - Same visualization as HTML reports (Mermaid diagrams)
9//! - No dependency on complex PDF generation libraries
10
11use super::html::HtmlReporter;
12use crate::core::report::{ReportContext, ReportGenerator};
13use std::error::Error;
14use std::path::Path;
15use std::process::Command;
16
17/// PDF report generator using HTML-to-PDF conversion
18pub struct PdfReporter {
19    /// Optional custom PDF converter command
20    converter: Option<String>,
21}
22
23impl PdfReporter {
24    /// Create a new PDF reporter
25    #[must_use]
26    pub const fn new() -> Self {
27        Self { converter: None }
28    }
29
30    /// Create a PDF reporter with a custom converter
31    #[must_use]
32    #[allow(clippy::missing_const_for_fn)]
33    pub fn with_converter(converter: &str) -> Self {
34        Self {
35            converter: Some(converter.to_owned()),
36        }
37    }
38
39    /// Detect available Chrome/Chromium browser
40    fn detect_chrome() -> Option<String> {
41        // Try common Chrome/Chromium executables in order of preference
42        let candidates = [
43            "google-chrome",
44            "chrome",
45            "chromium",
46            "chromium-browser",
47            "google-chrome-stable",
48            "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", // macOS
49            "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",   // Windows
50            "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
51        ];
52
53        for candidate in candidates {
54            if let Ok(output) = Command::new(candidate).arg("--version").output() {
55                if output.status.success() {
56                    return Some(candidate.to_owned());
57                }
58            }
59        }
60
61        None
62    }
63
64    /// Generate PDF from HTML file using Chrome/Chromium
65    fn html_to_pdf_chrome(
66        chrome_cmd: &str,
67        html_path: &Path,
68        pdf_path: &Path,
69    ) -> Result<(), Box<dyn Error>> {
70        // Suppress DBus warnings by redirecting stderr to /dev/null
71        use std::process::Stdio;
72
73        let status = Command::new(chrome_cmd)
74            .arg("--headless=new")
75            .arg("--disable-gpu")
76            .arg("--no-sandbox")
77            // Force complete rendering and JavaScript execution
78            .arg("--run-all-compositor-stages-before-draw")
79            // Extended timeout to ensure all JavaScript (including setTimeout) has time to complete
80            .arg("--virtual-time-budget=60000") // 60 seconds for JS and timeouts to complete
81            .arg("--disable-features=IsolateOrigins,site-per-process")
82            .arg("--enable-features=NetworkService,NetworkServiceInProcess")
83            // Force synchronous painting and wait for layout
84            .arg("--enable-automation")
85            // Ensure lazy loading doesn't interfere
86            .arg("--disable-lazy-loading")
87            .arg(format!("--print-to-pdf={}", pdf_path.display()))
88            .arg(format!("file://{}", html_path.canonicalize()?.display()))
89            .stderr(Stdio::null())
90            .stdout(Stdio::null())
91            .status()?;
92
93        if !status.success() {
94            return Err("Chrome PDF conversion failed".into());
95        }
96
97        Ok(())
98    }
99
100    /// Convert HTML report to PDF
101    fn convert_html_to_pdf(&self, html_path: &Path, pdf_path: &Path) -> Result<(), Box<dyn Error>> {
102        // Use custom converter if provided
103        if let Some(converter) = &self.converter {
104            return Self::html_to_pdf_chrome(converter, html_path, pdf_path);
105        }
106
107        // Try to auto-detect Chrome/Chromium
108        if let Some(chrome) = Self::detect_chrome() {
109            return Self::html_to_pdf_chrome(&chrome, html_path, pdf_path);
110        }
111
112        // No converter available
113        Err("PDF conversion failed: Chrome/Chromium not found.\n\
114            \n\
115            To generate PDF reports, install Chrome or Chromium:\n\
116            \n\
117            • Ubuntu/Debian:  sudo apt install chromium-browser\n\
118            • Fedora/RHEL:    sudo dnf install chromium\n\
119            • macOS:          brew install --cask google-chrome\n\
120            • Windows:        Download from https://www.google.com/chrome/\n\
121            \n\
122            Alternatively, specify a custom PDF converter:\n\
123              --pdf-converter /path/to/chrome\n\
124            "
125        .into())
126    }
127}
128
129impl Default for PdfReporter {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl ReportGenerator for PdfReporter {
136    /// Generate PDF report via HTML-to-PDF conversion
137    ///
138    /// First generates an HTML report, then converts it to PDF using
139    /// headless Chrome/Chromium or a specified converter.
140    fn generate(&self, ctx: &ReportContext, output_path: &Path) -> Result<(), Box<dyn Error>> {
141        // Generate HTML report to temporary file
142        let temp_dir = std::env::temp_dir();
143        let html_path = temp_dir.join(format!("nuanalytics_report_{}.html", std::process::id()));
144
145        let html_reporter = HtmlReporter::new();
146        html_reporter.generate(ctx, &html_path)?;
147
148        // Convert HTML to PDF
149        self.convert_html_to_pdf(&html_path, output_path)?;
150
151        // Clean up temporary HTML file
152        let _ = std::fs::remove_file(&html_path);
153
154        Ok(())
155    }
156
157    /// Render method for consistency with other reporters
158    fn render(&self, _ctx: &ReportContext) -> Result<String, Box<dyn Error>> {
159        Ok(String::from(
160            "PDF reports are generated via HTML-to-PDF conversion.",
161        ))
162    }
163}