html2pdf_secure/
converter.rs

1//! HTML to PDF conversion functionality using headless Chrome.
2
3use crate::error::{Html2PdfError, Result};
4use crate::encryption::{encrypt_pdf, encrypt_pdf_with_qpdf, PasswordOptions};
5use crate::options::{PdfOptions, PageOrientation};
6use headless_chrome::types::PrintToPdfOptions;
7use headless_chrome::{Browser, LaunchOptions, Tab};
8use std::sync::Arc;
9use std::time::Duration;
10
11/// Main converter for HTML to PDF with password protection.
12pub struct Html2PdfConverter {
13    browser: Browser,
14}
15
16impl Html2PdfConverter {
17    /// Create a new converter instance.
18    pub async fn new() -> Result<Self> {
19        Self::with_options(LaunchOptions::default()).await
20    }
21
22    /// Create a new converter with custom browser launch options.
23    pub async fn with_options(launch_options: LaunchOptions<'_>) -> Result<Self> {
24        let browser = Browser::new(launch_options)?;
25        Ok(Self { browser })
26    }
27
28    /// Convert HTML string to PDF bytes.
29    pub async fn convert_html_to_pdf(&self, html: &str, options: PdfOptions) -> Result<Vec<u8>> {
30        let tab = self.browser.new_tab()?;
31        self.setup_tab(&tab, &options).await?;
32        
33        // Navigate to data URL with HTML content
34        let data_url = format!("data:text/html;charset=utf-8,{}", urlencoding::encode(html));
35        tab.navigate_to(&data_url)?;
36        
37        // Wait for page to load
38        self.wait_for_page_load(&tab, &options).await?;
39        
40        // Generate PDF
41        let pdf_data = self.generate_pdf(&tab, &options).await?;
42        
43        Ok(pdf_data)
44    }
45
46    /// Convert HTML file to PDF bytes.
47    pub async fn convert_html_file_to_pdf(&self, file_path: &str, options: PdfOptions) -> Result<Vec<u8>> {
48        let html = std::fs::read_to_string(file_path)
49            .map_err(|e| Html2PdfError::invalid_input(format!("Failed to read HTML file: {}", e)))?;
50        
51        self.convert_html_to_pdf(&html, options).await
52    }
53
54    /// Convert URL to PDF bytes.
55    pub async fn convert_url_to_pdf(&self, url: &str, options: PdfOptions) -> Result<Vec<u8>> {
56        let tab = self.browser.new_tab()?;
57        self.setup_tab(&tab, &options).await?;
58        
59        // Navigate to URL
60        tab.navigate_to(url)?;
61        
62        // Wait for page to load
63        self.wait_for_page_load(&tab, &options).await?;
64        
65        // Generate PDF
66        let pdf_data = self.generate_pdf(&tab, &options).await?;
67        
68        Ok(pdf_data)
69    }
70
71    /// Convert HTML to password-protected PDF.
72    ///
73    /// This method attempts to use qpdf for proper encryption. If qpdf is not available,
74    /// it will return an unencrypted PDF with warnings. For guaranteed encryption,
75    /// use `convert_html_to_protected_pdf_with_qpdf` and handle the error appropriately.
76    pub async fn convert_html_to_protected_pdf(
77        &self,
78        html: &str,
79        pdf_options: PdfOptions,
80        password_options: PasswordOptions,
81    ) -> Result<Vec<u8>> {
82        // First convert to PDF
83        let pdf_data = self.convert_html_to_pdf(html, pdf_options).await?;
84
85        // Then encrypt the PDF (tries qpdf first, falls back to unencrypted)
86        encrypt_pdf(pdf_data, &password_options)
87    }
88
89    /// Convert HTML file to password-protected PDF.
90    pub async fn convert_html_file_to_protected_pdf(
91        &self,
92        file_path: &str,
93        pdf_options: PdfOptions,
94        password_options: PasswordOptions,
95    ) -> Result<Vec<u8>> {
96        // First convert to PDF
97        let pdf_data = self.convert_html_file_to_pdf(file_path, pdf_options).await?;
98        
99        // Then encrypt the PDF
100        encrypt_pdf(pdf_data, &password_options)
101    }
102
103    /// Convert URL to password-protected PDF.
104    pub async fn convert_url_to_protected_pdf(
105        &self,
106        url: &str,
107        pdf_options: PdfOptions,
108        password_options: PasswordOptions,
109    ) -> Result<Vec<u8>> {
110        // First convert to PDF
111        let pdf_data = self.convert_url_to_pdf(url, pdf_options).await?;
112
113        // Then encrypt the PDF
114        encrypt_pdf(pdf_data, &password_options)
115    }
116
117    /// Convert HTML to password-protected PDF using qpdf (if available).
118    /// This provides stronger encryption than the basic implementation.
119    pub async fn convert_html_to_protected_pdf_with_qpdf(
120        &self,
121        html: &str,
122        pdf_options: PdfOptions,
123        password_options: PasswordOptions,
124        temp_dir: Option<&str>,
125    ) -> Result<Vec<u8>> {
126        // First convert to PDF
127        let pdf_data = self.convert_html_to_pdf(html, pdf_options).await?;
128
129        // Then encrypt the PDF using qpdf
130        encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
131    }
132
133    /// Convert HTML file to password-protected PDF using qpdf (if available).
134    pub async fn convert_html_file_to_protected_pdf_with_qpdf(
135        &self,
136        file_path: &str,
137        pdf_options: PdfOptions,
138        password_options: PasswordOptions,
139        temp_dir: Option<&str>,
140    ) -> Result<Vec<u8>> {
141        // First convert to PDF
142        let pdf_data = self.convert_html_file_to_pdf(file_path, pdf_options).await?;
143
144        // Then encrypt the PDF using qpdf
145        encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
146    }
147
148    /// Convert URL to password-protected PDF using qpdf (if available).
149    pub async fn convert_url_to_protected_pdf_with_qpdf(
150        &self,
151        url: &str,
152        pdf_options: PdfOptions,
153        password_options: PasswordOptions,
154        temp_dir: Option<&str>,
155    ) -> Result<Vec<u8>> {
156        // First convert to PDF
157        let pdf_data = self.convert_url_to_pdf(url, pdf_options).await?;
158
159        // Then encrypt the PDF using qpdf
160        encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
161    }
162
163    /// Setup tab with initial configuration.
164    async fn setup_tab(&self, _tab: &Arc<Tab>, _options: &PdfOptions) -> Result<()> {
165        // Note: headless_chrome doesn't expose set_viewport method directly
166        // The viewport is typically handled through the print_to_pdf options
167        Ok(())
168    }
169
170
171
172    /// Wait for page to load completely.
173    async fn wait_for_page_load(&self, tab: &Arc<Tab>, options: &PdfOptions) -> Result<()> {
174        // Wait for page load event
175        tab.wait_until_navigated()?;
176        
177        // Wait for network idle if requested
178        if options.wait_for_network_idle {
179            tab.wait_for_element_with_custom_timeout("body", Duration::from_secs(options.timeout_seconds))?;
180        }
181        
182        // Additional wait time
183        if options.additional_wait_ms > 0 {
184            tokio::time::sleep(Duration::from_millis(options.additional_wait_ms)).await;
185        }
186        
187        Ok(())
188    }
189
190    /// Generate PDF from the current tab.
191    async fn generate_pdf(&self, tab: &Arc<Tab>, options: &PdfOptions) -> Result<Vec<u8>> {
192        let (width, height) = self.calculate_page_size(options);
193
194        let pdf_options = PrintToPdfOptions {
195            landscape: Some(matches!(options.orientation, PageOrientation::Landscape)),
196            display_header_footer: Some(false),
197            print_background: Some(options.print_background),
198            scale: Some(options.scale),
199            paper_width: Some(width),
200            paper_height: Some(height),
201            margin_top: Some(options.margins.top),
202            margin_bottom: Some(options.margins.bottom),
203            margin_left: Some(options.margins.left),
204            margin_right: Some(options.margins.right),
205            page_ranges: None,
206            ignore_invalid_page_ranges: Some(false),
207            header_template: None,
208            footer_template: None,
209            prefer_css_page_size: Some(options.prefer_css_page_size),
210            transfer_mode: None,
211            generate_document_outline: Some(false),
212            generate_tagged_pdf: Some(false),
213        };
214
215        let pdf_data = tab.print_to_pdf(Some(pdf_options))?;
216        Ok(pdf_data)
217    }
218
219    /// Calculate page size in inches.
220    fn calculate_page_size(&self, options: &PdfOptions) -> (f64, f64) {
221        if let (Some(w), Some(h)) = (options.custom_width, options.custom_height) {
222            (w, h)
223        } else {
224            let (mut width, mut height) = options.page_format.dimensions();
225            
226            // Swap dimensions for landscape
227            if matches!(options.orientation, PageOrientation::Landscape) {
228                std::mem::swap(&mut width, &mut height);
229            }
230            
231            (width, height)
232        }
233    }
234}