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)]
17#[derive(Default)]
18pub enum PaperFormat {
19 #[default]
21 Letter,
22 Legal,
24 Tabloid,
26 Ledger,
28 A0,
30 A1,
32 A2,
34 A3,
36 A4,
38 A5,
40 A6,
42 Custom { width: f64, height: f64 },
44}
45
46impl PaperFormat {
47 pub fn width(&self) -> f64 {
49 match self {
50 PaperFormat::Letter => 8.5,
51 PaperFormat::Legal => 8.5,
52 PaperFormat::Tabloid => 11.0,
53 PaperFormat::Ledger => 17.0,
54 PaperFormat::A0 => 33.1,
55 PaperFormat::A1 => 23.4,
56 PaperFormat::A2 => 16.5,
57 PaperFormat::A3 => 11.7,
58 PaperFormat::A4 => 8.27,
59 PaperFormat::A5 => 5.83,
60 PaperFormat::A6 => 4.13,
61 PaperFormat::Custom { width, .. } => *width,
62 }
63 }
64
65 pub fn height(&self) -> f64 {
67 match self {
68 PaperFormat::Letter => 11.0,
69 PaperFormat::Legal => 14.0,
70 PaperFormat::Tabloid => 17.0,
71 PaperFormat::Ledger => 11.0,
72 PaperFormat::A0 => 46.8,
73 PaperFormat::A1 => 33.1,
74 PaperFormat::A2 => 23.4,
75 PaperFormat::A3 => 16.5,
76 PaperFormat::A4 => 11.69,
77 PaperFormat::A5 => 8.27,
78 PaperFormat::A6 => 5.83,
79 PaperFormat::Custom { height, .. } => *height,
80 }
81 }
82}
83
84
85#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct Margins {
88 pub top: f64,
90 pub right: f64,
92 pub bottom: f64,
94 pub left: f64,
96}
97
98impl Margins {
99 pub fn uniform(margin: f64) -> Self {
101 Self {
102 top: margin,
103 right: margin,
104 bottom: margin,
105 left: margin,
106 }
107 }
108
109 pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
111 Self {
112 top: vertical,
113 right: horizontal,
114 bottom: vertical,
115 left: horizontal,
116 }
117 }
118
119 pub fn new(top: f64, right: f64, bottom: f64, left: f64) -> Self {
121 Self {
122 top,
123 right,
124 bottom,
125 left,
126 }
127 }
128}
129
130impl Default for Margins {
131 fn default() -> Self {
132 Self {
134 top: 0.4,
135 right: 0.4,
136 bottom: 0.4,
137 left: 0.4,
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct PdfBuilder<'a> {
145 page: &'a Page,
146 format: PaperFormat,
147 landscape: bool,
148 margins: Margins,
149 scale: f64,
150 print_background: bool,
151 header_template: Option<String>,
152 footer_template: Option<String>,
153 page_ranges: Option<String>,
154 prefer_css_page_size: bool,
155 path: Option<String>,
156}
157
158impl<'a> PdfBuilder<'a> {
159 pub(crate) fn new(page: &'a Page) -> Self {
161 Self {
162 page,
163 format: PaperFormat::default(),
164 landscape: false,
165 margins: Margins::default(),
166 scale: 1.0,
167 print_background: false,
168 header_template: None,
169 footer_template: None,
170 page_ranges: None,
171 prefer_css_page_size: false,
172 path: None,
173 }
174 }
175
176 #[must_use]
178 pub fn format(mut self, format: PaperFormat) -> Self {
179 self.format = format;
180 self
181 }
182
183 #[must_use]
185 pub fn landscape(mut self, landscape: bool) -> Self {
186 self.landscape = landscape;
187 self
188 }
189
190 #[must_use]
192 pub fn margins(mut self, margins: Margins) -> Self {
193 self.margins = margins;
194 self
195 }
196
197 #[must_use]
199 pub fn margin(mut self, margin: f64) -> Self {
200 self.margins = Margins::uniform(margin);
201 self
202 }
203
204 #[must_use]
206 pub fn margin_all(mut self, top: f64, right: f64, bottom: f64, left: f64) -> Self {
207 self.margins = Margins::new(top, right, bottom, left);
208 self
209 }
210
211 #[must_use]
213 pub fn scale(mut self, scale: f64) -> Self {
214 self.scale = scale.clamp(0.1, 2.0);
215 self
216 }
217
218 #[must_use]
220 pub fn print_background(mut self, print_background: bool) -> Self {
221 self.print_background = print_background;
222 self
223 }
224
225 #[must_use]
234 pub fn header_template(mut self, template: impl Into<String>) -> Self {
235 self.header_template = Some(template.into());
236 self
237 }
238
239 #[must_use]
243 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
244 self.footer_template = Some(template.into());
245 self
246 }
247
248 #[must_use]
250 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
251 self.page_ranges = Some(ranges.into());
252 self
253 }
254
255 #[must_use]
257 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
258 self.prefer_css_page_size = prefer;
259 self
260 }
261
262 #[must_use]
264 pub fn path(mut self, path: impl AsRef<Path>) -> Self {
265 self.path = Some(path.as_ref().to_string_lossy().to_string());
266 self
267 }
268
269 #[instrument(level = "info", skip(self), fields(format = ?self.format, landscape = self.landscape, has_path = self.path.is_some()))]
280 pub async fn generate(self) -> Result<Vec<u8>, PageError> {
281 if self.page.is_closed() {
282 return Err(PageError::Closed);
283 }
284
285 info!("Generating PDF");
286
287 let display_header_footer = self.header_template.is_some() || self.footer_template.is_some();
288
289 let params = PrintToPdfParams {
290 landscape: Some(self.landscape),
291 display_header_footer: Some(display_header_footer),
292 print_background: Some(self.print_background),
293 scale: Some(self.scale),
294 paper_width: Some(self.format.width()),
295 paper_height: Some(self.format.height()),
296 margin_top: Some(self.margins.top),
297 margin_bottom: Some(self.margins.bottom),
298 margin_left: Some(self.margins.left),
299 margin_right: Some(self.margins.right),
300 page_ranges: self.page_ranges.clone(),
301 header_template: self.header_template.clone(),
302 footer_template: self.footer_template.clone(),
303 prefer_css_page_size: Some(self.prefer_css_page_size),
304 transfer_mode: None,
305 generate_tagged_pdf: None,
306 generate_document_outline: None,
307 };
308
309 debug!("Sending Page.printToPDF command");
310 let result: PrintToPdfResult = self
311 .page
312 .connection()
313 .send_command("Page.printToPDF", Some(params), Some(self.page.session_id()))
314 .await?;
315
316 let data = base64_decode(&result.data)?;
318 debug!(bytes = data.len(), "PDF generated");
319
320 if let Some(ref path) = self.path {
322 debug!(path = path, "Saving PDF to file");
323 tokio::fs::write(path, &data).await.map_err(|e| {
324 PageError::EvaluationFailed(format!("Failed to save PDF: {e}"))
325 })?;
326 info!(path = path, "PDF saved");
327 }
328
329 Ok(data)
330 }
331}