use std::fs;
use std::path::Path;
use headless_chrome::{Browser, LaunchOptions};
use headless_chrome::types::PrintToPdfOptions;
pub struct PdfOptions {
pub margin_top: f64,
pub margin_bottom: f64,
pub margin_left: f64,
pub margin_right: f64,
pub footer_template: Option<String>,
}
impl Default for PdfOptions {
fn default() -> Self {
Self {
margin_top: 2.0,
margin_bottom: 2.5,
margin_left: 1.8,
margin_right: 1.8,
footer_template: None,
}
}
}
const CM_TO_INCHES: f64 = 0.3937;
pub fn html_to_pdf(html: &str, output: &Path, options: &PdfOptions) -> Result<u64, RenderError> {
let temp_dir = tempfile::tempdir()
.map_err(|e| RenderError::Io(format!("failed to create temp dir: {e}")))?;
let html_path = temp_dir.path().join("document.html");
fs::write(&html_path, html)
.map_err(|e| RenderError::Io(format!("failed to write temp HTML: {e}")))?;
let browser = Browser::new(LaunchOptions {
headless: true,
sandbox: false,
..Default::default()
})
.map_err(|e| RenderError::Chrome(format!("failed to launch Chrome: {e}")))?;
let tab = browser
.new_tab()
.map_err(|e| RenderError::Chrome(format!("failed to create tab: {e}")))?;
let file_url = format!("file://{}", html_path.display());
tab.navigate_to(&file_url)
.map_err(|e| RenderError::Chrome(format!("failed to navigate: {e}")))?;
tab.wait_until_navigated()
.map_err(|e| RenderError::Chrome(format!("failed to wait for navigation: {e}")))?;
let footer = options.footer_template.clone().unwrap_or_else(|| {
r#"<div style="width:100%;text-align:center;font-size:9px;color:#888;font-family:sans-serif;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>"#.to_owned()
});
let pdf_data = tab
.print_to_pdf(Some(PrintToPdfOptions {
landscape: Some(false),
display_header_footer: Some(true),
print_background: Some(true),
scale: Some(1.0),
paper_width: Some(8.27), paper_height: Some(11.69), margin_top: Some(options.margin_top * CM_TO_INCHES),
margin_bottom: Some(options.margin_bottom * CM_TO_INCHES),
margin_left: Some(options.margin_left * CM_TO_INCHES),
margin_right: Some(options.margin_right * CM_TO_INCHES),
header_template: Some("<span></span>".to_owned()),
footer_template: Some(footer),
page_ranges: None,
ignore_invalid_page_ranges: None,
prefer_css_page_size: None,
transfer_mode: None,
generate_tagged_pdf: None,
generate_document_outline: None,
}))
.map_err(|e| RenderError::Chrome(format!("failed to print PDF: {e}")))?;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|e| RenderError::Io(format!("failed to create output dir: {e}")))?;
}
fs::write(output, &pdf_data)
.map_err(|e| RenderError::Io(format!("failed to write PDF: {e}")))?;
Ok(pdf_data.len() as u64)
}
#[derive(Debug)]
pub enum RenderError {
Io(String),
Chrome(String),
}
impl std::fmt::Display for RenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RenderError::Io(msg) => write!(f, "IO error: {msg}"),
RenderError::Chrome(msg) => write!(f, "Chrome error: {msg}"),
}
}
}
impl std::error::Error for RenderError {}