1use 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
11pub struct Html2PdfConverter {
13 browser: Browser,
14}
15
16impl Html2PdfConverter {
17 pub async fn new() -> Result<Self> {
19 Self::with_options(LaunchOptions::default()).await
20 }
21
22 pub async fn with_options(launch_options: LaunchOptions<'_>) -> Result<Self> {
24 let browser = Browser::new(launch_options)?;
25 Ok(Self { browser })
26 }
27
28 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 let data_url = format!("data:text/html;charset=utf-8,{}", urlencoding::encode(html));
35 tab.navigate_to(&data_url)?;
36
37 self.wait_for_page_load(&tab, &options).await?;
39
40 let pdf_data = self.generate_pdf(&tab, &options).await?;
42
43 Ok(pdf_data)
44 }
45
46 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 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 tab.navigate_to(url)?;
61
62 self.wait_for_page_load(&tab, &options).await?;
64
65 let pdf_data = self.generate_pdf(&tab, &options).await?;
67
68 Ok(pdf_data)
69 }
70
71 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 let pdf_data = self.convert_html_to_pdf(html, pdf_options).await?;
84
85 encrypt_pdf(pdf_data, &password_options)
87 }
88
89 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 let pdf_data = self.convert_html_file_to_pdf(file_path, pdf_options).await?;
98
99 encrypt_pdf(pdf_data, &password_options)
101 }
102
103 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 let pdf_data = self.convert_url_to_pdf(url, pdf_options).await?;
112
113 encrypt_pdf(pdf_data, &password_options)
115 }
116
117 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 let pdf_data = self.convert_html_to_pdf(html, pdf_options).await?;
128
129 encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
131 }
132
133 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 let pdf_data = self.convert_html_file_to_pdf(file_path, pdf_options).await?;
143
144 encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
146 }
147
148 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 let pdf_data = self.convert_url_to_pdf(url, pdf_options).await?;
158
159 encrypt_pdf_with_qpdf(pdf_data, &password_options, temp_dir)
161 }
162
163 async fn setup_tab(&self, _tab: &Arc<Tab>, _options: &PdfOptions) -> Result<()> {
165 Ok(())
168 }
169
170
171
172 async fn wait_for_page_load(&self, tab: &Arc<Tab>, options: &PdfOptions) -> Result<()> {
174 tab.wait_until_navigated()?;
176
177 if options.wait_for_network_idle {
179 tab.wait_for_element_with_custom_timeout("body", Duration::from_secs(options.timeout_seconds))?;
180 }
181
182 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 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 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 if matches!(options.orientation, PageOrientation::Landscape) {
228 std::mem::swap(&mut width, &mut height);
229 }
230
231 (width, height)
232 }
233 }
234}