1use std::fs;
2use std::path::Path;
3
4use headless_chrome::{Browser, LaunchOptions};
5use headless_chrome::types::PrintToPdfOptions;
6
7pub struct PdfOptions {
9 pub margin_top: f64,
11 pub margin_bottom: f64,
13 pub margin_left: f64,
15 pub margin_right: f64,
17 pub footer_template: Option<String>,
19}
20
21impl Default for PdfOptions {
22 fn default() -> Self {
23 Self {
24 margin_top: 2.0,
25 margin_bottom: 2.5,
26 margin_left: 1.8,
27 margin_right: 1.8,
28 footer_template: None,
29 }
30 }
31}
32
33const CM_TO_INCHES: f64 = 0.3937;
34
35pub fn html_to_pdf(html: &str, output: &Path, options: &PdfOptions) -> Result<u64, RenderError> {
40 let temp_dir = tempfile::tempdir()
42 .map_err(|e| RenderError::Io(format!("failed to create temp dir: {e}")))?;
43 let html_path = temp_dir.path().join("document.html");
44 fs::write(&html_path, html)
45 .map_err(|e| RenderError::Io(format!("failed to write temp HTML: {e}")))?;
46
47 let browser = Browser::new(LaunchOptions {
48 headless: true,
49 sandbox: false,
50 ..Default::default()
51 })
52 .map_err(|e| RenderError::Chrome(format!("failed to launch Chrome: {e}")))?;
53
54 let tab = browser
55 .new_tab()
56 .map_err(|e| RenderError::Chrome(format!("failed to create tab: {e}")))?;
57
58 let file_url = format!("file://{}", html_path.display());
59 tab.navigate_to(&file_url)
60 .map_err(|e| RenderError::Chrome(format!("failed to navigate: {e}")))?;
61
62 tab.wait_until_navigated()
63 .map_err(|e| RenderError::Chrome(format!("failed to wait for navigation: {e}")))?;
64
65 let footer = options.footer_template.clone().unwrap_or_else(|| {
66 r#"<div style="width:100%;text-align:center;font-size:9px;color:#888;font-family:sans-serif;">
67 <span class="pageNumber"></span> / <span class="totalPages"></span>
68 </div>"#.to_owned()
69 });
70
71 let pdf_data = tab
72 .print_to_pdf(Some(PrintToPdfOptions {
73 landscape: Some(false),
74 display_header_footer: Some(true),
75 print_background: Some(true),
76 scale: Some(1.0),
77 paper_width: Some(8.27), paper_height: Some(11.69), margin_top: Some(options.margin_top * CM_TO_INCHES),
80 margin_bottom: Some(options.margin_bottom * CM_TO_INCHES),
81 margin_left: Some(options.margin_left * CM_TO_INCHES),
82 margin_right: Some(options.margin_right * CM_TO_INCHES),
83 header_template: Some("<span></span>".to_owned()),
84 footer_template: Some(footer),
85 page_ranges: None,
86 ignore_invalid_page_ranges: None,
87 prefer_css_page_size: None,
88 transfer_mode: None,
89 generate_tagged_pdf: None,
90 generate_document_outline: None,
91 }))
92 .map_err(|e| RenderError::Chrome(format!("failed to print PDF: {e}")))?;
93
94 if let Some(parent) = output.parent() {
96 fs::create_dir_all(parent)
97 .map_err(|e| RenderError::Io(format!("failed to create output dir: {e}")))?;
98 }
99
100 fs::write(output, &pdf_data)
101 .map_err(|e| RenderError::Io(format!("failed to write PDF: {e}")))?;
102
103 Ok(pdf_data.len() as u64)
104}
105
106#[derive(Debug)]
107pub enum RenderError {
108 Io(String),
109 Chrome(String),
110}
111
112impl std::fmt::Display for RenderError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 RenderError::Io(msg) => write!(f, "IO error: {msg}"),
116 RenderError::Chrome(msg) => write!(f, "Chrome error: {msg}"),
117 }
118 }
119}
120
121impl std::error::Error for RenderError {}