1use std::path::Path;
6
7use tracing::{debug, info, instrument};
8use viewpoint_cdp::protocol::page::{PrintToPdfParams, PrintToPdfResult};
9
10use crate::error::PageError;
11
12use super::Page;
13use super::screenshot::base64_decode;
14
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
17pub enum PaperFormat {
18 #[default]
20 Letter,
21 Legal,
23 Tabloid,
25 Ledger,
27 A0,
29 A1,
31 A2,
33 A3,
35 A4,
37 A5,
39 A6,
41 Custom { width: f64, height: f64 },
43}
44
45impl PaperFormat {
46 pub fn width(&self) -> f64 {
48 match self {
49 PaperFormat::Letter => 8.5,
50 PaperFormat::Legal => 8.5,
51 PaperFormat::Tabloid => 11.0,
52 PaperFormat::Ledger => 17.0,
53 PaperFormat::A0 => 33.1,
54 PaperFormat::A1 => 23.4,
55 PaperFormat::A2 => 16.5,
56 PaperFormat::A3 => 11.7,
57 PaperFormat::A4 => 8.27,
58 PaperFormat::A5 => 5.83,
59 PaperFormat::A6 => 4.13,
60 PaperFormat::Custom { width, .. } => *width,
61 }
62 }
63
64 pub fn height(&self) -> f64 {
66 match self {
67 PaperFormat::Letter => 11.0,
68 PaperFormat::Legal => 14.0,
69 PaperFormat::Tabloid => 17.0,
70 PaperFormat::Ledger => 11.0,
71 PaperFormat::A0 => 46.8,
72 PaperFormat::A1 => 33.1,
73 PaperFormat::A2 => 23.4,
74 PaperFormat::A3 => 16.5,
75 PaperFormat::A4 => 11.69,
76 PaperFormat::A5 => 8.27,
77 PaperFormat::A6 => 5.83,
78 PaperFormat::Custom { height, .. } => *height,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct Margins {
86 pub top: f64,
88 pub right: f64,
90 pub bottom: f64,
92 pub left: f64,
94}
95
96impl Margins {
97 pub fn uniform(margin: f64) -> Self {
99 Self {
100 top: margin,
101 right: margin,
102 bottom: margin,
103 left: margin,
104 }
105 }
106
107 pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
109 Self {
110 top: vertical,
111 right: horizontal,
112 bottom: vertical,
113 left: horizontal,
114 }
115 }
116
117 pub fn new(top: f64, right: f64, bottom: f64, left: f64) -> Self {
119 Self {
120 top,
121 right,
122 bottom,
123 left,
124 }
125 }
126}
127
128impl Default for Margins {
129 fn default() -> Self {
130 Self {
132 top: 0.4,
133 right: 0.4,
134 bottom: 0.4,
135 left: 0.4,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct PdfBuilder<'a> {
143 page: &'a Page,
144 format: PaperFormat,
145 landscape: bool,
146 margins: Margins,
147 scale: f64,
148 print_background: bool,
149 header_template: Option<String>,
150 footer_template: Option<String>,
151 page_ranges: Option<String>,
152 prefer_css_page_size: bool,
153 path: Option<String>,
154}
155
156impl<'a> PdfBuilder<'a> {
157 pub(crate) fn new(page: &'a Page) -> Self {
159 Self {
160 page,
161 format: PaperFormat::default(),
162 landscape: false,
163 margins: Margins::default(),
164 scale: 1.0,
165 print_background: false,
166 header_template: None,
167 footer_template: None,
168 page_ranges: None,
169 prefer_css_page_size: false,
170 path: None,
171 }
172 }
173
174 #[must_use]
176 pub fn format(mut self, format: PaperFormat) -> Self {
177 self.format = format;
178 self
179 }
180
181 #[must_use]
183 pub fn landscape(mut self, landscape: bool) -> Self {
184 self.landscape = landscape;
185 self
186 }
187
188 #[must_use]
190 pub fn margins(mut self, margins: Margins) -> Self {
191 self.margins = margins;
192 self
193 }
194
195 #[must_use]
197 pub fn margin(mut self, margin: f64) -> Self {
198 self.margins = Margins::uniform(margin);
199 self
200 }
201
202 #[must_use]
204 pub fn margin_all(mut self, top: f64, right: f64, bottom: f64, left: f64) -> Self {
205 self.margins = Margins::new(top, right, bottom, left);
206 self
207 }
208
209 #[must_use]
211 pub fn scale(mut self, scale: f64) -> Self {
212 self.scale = scale.clamp(0.1, 2.0);
213 self
214 }
215
216 #[must_use]
218 pub fn print_background(mut self, print_background: bool) -> Self {
219 self.print_background = print_background;
220 self
221 }
222
223 #[must_use]
232 pub fn header_template(mut self, template: impl Into<String>) -> Self {
233 self.header_template = Some(template.into());
234 self
235 }
236
237 #[must_use]
241 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
242 self.footer_template = Some(template.into());
243 self
244 }
245
246 #[must_use]
248 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
249 self.page_ranges = Some(ranges.into());
250 self
251 }
252
253 #[must_use]
255 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
256 self.prefer_css_page_size = prefer;
257 self
258 }
259
260 #[must_use]
262 pub fn path(mut self, path: impl AsRef<Path>) -> Self {
263 self.path = Some(path.as_ref().to_string_lossy().to_string());
264 self
265 }
266
267 #[instrument(level = "info", skip(self), fields(format = ?self.format, landscape = self.landscape, has_path = self.path.is_some()))]
278 pub async fn generate(self) -> Result<Vec<u8>, PageError> {
279 if self.page.is_closed() {
280 return Err(PageError::Closed);
281 }
282
283 info!("Generating PDF");
284
285 let display_header_footer =
286 self.header_template.is_some() || self.footer_template.is_some();
287
288 let params = PrintToPdfParams {
289 landscape: Some(self.landscape),
290 display_header_footer: Some(display_header_footer),
291 print_background: Some(self.print_background),
292 scale: Some(self.scale),
293 paper_width: Some(self.format.width()),
294 paper_height: Some(self.format.height()),
295 margin_top: Some(self.margins.top),
296 margin_bottom: Some(self.margins.bottom),
297 margin_left: Some(self.margins.left),
298 margin_right: Some(self.margins.right),
299 page_ranges: self.page_ranges.clone(),
300 header_template: self.header_template.clone(),
301 footer_template: self.footer_template.clone(),
302 prefer_css_page_size: Some(self.prefer_css_page_size),
303 transfer_mode: None,
304 generate_tagged_pdf: None,
305 generate_document_outline: None,
306 };
307
308 debug!("Sending Page.printToPDF command");
309 let result: PrintToPdfResult = self
310 .page
311 .connection()
312 .send_command(
313 "Page.printToPDF",
314 Some(params),
315 Some(self.page.session_id()),
316 )
317 .await?;
318
319 let data = base64_decode(&result.data)?;
321 debug!(bytes = data.len(), "PDF generated");
322
323 if let Some(ref path) = self.path {
325 debug!(path = path, "Saving PDF to file");
326 tokio::fs::write(path, &data)
327 .await
328 .map_err(|e| PageError::EvaluationFailed(format!("Failed to save PDF: {e}")))?;
329 info!(path = path, "PDF saved");
330 }
331
332 Ok(data)
333 }
334}