nu_analytics/core/report/formats/
pdf.rs1use super::html::HtmlReporter;
12use crate::core::report::{ReportContext, ReportGenerator};
13use std::error::Error;
14use std::path::Path;
15use std::process::Command;
16
17pub struct PdfReporter {
19 converter: Option<String>,
21}
22
23impl PdfReporter {
24 #[must_use]
26 pub const fn new() -> Self {
27 Self { converter: None }
28 }
29
30 #[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 fn detect_chrome() -> Option<String> {
41 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", "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "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 fn html_to_pdf_chrome(
66 chrome_cmd: &str,
67 html_path: &Path,
68 pdf_path: &Path,
69 ) -> Result<(), Box<dyn Error>> {
70 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 .arg("--run-all-compositor-stages-before-draw")
79 .arg("--virtual-time-budget=60000") .arg("--disable-features=IsolateOrigins,site-per-process")
82 .arg("--enable-features=NetworkService,NetworkServiceInProcess")
83 .arg("--enable-automation")
85 .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 fn convert_html_to_pdf(&self, html_path: &Path, pdf_path: &Path) -> Result<(), Box<dyn Error>> {
102 if let Some(converter) = &self.converter {
104 return Self::html_to_pdf_chrome(converter, html_path, pdf_path);
105 }
106
107 if let Some(chrome) = Self::detect_chrome() {
109 return Self::html_to_pdf_chrome(&chrome, html_path, pdf_path);
110 }
111
112 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 fn generate(&self, ctx: &ReportContext, output_path: &Path) -> Result<(), Box<dyn Error>> {
141 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 self.convert_html_to_pdf(&html_path, output_path)?;
150
151 let _ = std::fs::remove_file(&html_path);
153
154 Ok(())
155 }
156
157 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}