Skip to main content

web2pdf_lib/
lib.rs

1use std::future::Future;
2use std::path::Path;
3use tokio::fs;
4
5use chromiumoxide::cdp::browser_protocol::page::PrintToPdfParams;
6use chromiumoxide::cdp::browser_protocol::target::CreateTargetParams;
7use chromiumoxide::handler::viewport::Viewport;
8use chromiumoxide::page::MediaTypeParams;
9use chromiumoxide::Page;
10use futures::StreamExt;
11
12pub use chromiumoxide::browser::Browser;
13pub use chromiumoxide::browser::BrowserConfig;
14pub mod util;
15
16pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
17
18pub trait BrowserWeb2Pdf {
19    fn web2pdf_launch_from_config(
20        browser_config: BrowserConfig,
21    ) -> impl Future<Output = Result<Browser>> + Send;
22    fn web2pdf_launch() -> impl Future<Output = Result<Browser>> + Send;
23    fn web2pdf_launch_from_executable_path(
24        path: impl AsRef<Path> + Send,
25    ) -> impl Future<Output = Result<Browser>> + Send;
26    fn close_and_wait(self) -> impl Future<Output = Result<Browser>> + Send;
27    fn web2pdf_new_page(
28        &self,
29        params: impl Into<CreateTargetParams> + Send,
30    ) -> impl Future<Output = Result<Page>> + Send;
31    fn web2pdf_load_cookie_file(
32        &self,
33        file: impl AsRef<Path> + Send,
34    ) -> impl Future<Output = Result<()>> + Send;
35}
36
37pub trait PageWeb2Pdf {
38    fn web2pdf_save_pdf_standard(
39        &self,
40        output: impl AsRef<Path> + Send,
41    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send;
42    fn web2pdf_save_pdf_mono(
43        &self,
44        opts: PrintToPdfParams,
45        output: impl AsRef<Path> + Send,
46    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send;
47    fn web2pdf_save_pdf_mono_standard(
48        &self,
49        output: impl AsRef<Path> + Send,
50    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send;
51}
52
53pub trait ViewportWeb2Pdf {
54    fn web2pdf_viewport() -> Viewport;
55}
56
57impl BrowserWeb2Pdf for Browser {
58    /// Creates a new `Browser` instance from a given `BrowserConfig`.
59    /// Please remeber to use Viewport::web2pdf_viewport() for building the config
60    ///
61    /// # Arguments
62    /// * `browser_config` - The `BrowserConfig` to use for launching the browser.
63    ///
64    /// # Returns
65    /// A `Result` containing a new `Web2Pdf` instance or an error.
66    fn web2pdf_launch_from_config(
67        browser_config: BrowserConfig,
68    ) -> impl Future<Output = Result<Browser>> + Send {
69        async {
70            let (browser, mut handler) = Browser::launch(browser_config).await?;
71
72            // Spawn a task to handle the browser events
73            let _ = tokio::spawn(async move { while let Some(_) = handler.next().await {} });
74
75            tracing::debug!("Web2Pdf browser launched");
76
77            Ok(browser)
78        }
79    }
80
81    /// Creates a new `Browser` instance using the system's installed Chromium browser.
82    ///
83    /// # Returns
84    /// A `Result` containing a new `Browser` instance or an error.
85    fn web2pdf_launch() -> impl Future<Output = Result<Browser>> + Send {
86        async {
87            // Attempt to find a system installation of chromium
88            let browser_config = BrowserConfig::builder()
89                .viewport(Some(Viewport::web2pdf_viewport()))
90                .build()?;
91
92            tracing::debug!("Web2Pdf browser launching using standard config");
93
94            Self::web2pdf_launch_from_config(browser_config).await
95        }
96    }
97
98    /// Creates a new `Browser` instance using a specific Chromium executable path.
99    ///
100    /// # Arguments
101    /// * `path` - A path to the Chromium executable.
102    ///
103    /// # Returns
104    /// A `Result` containing a new `Browser` instance or an error.
105    fn web2pdf_launch_from_executable_path(
106        path: impl AsRef<Path> + Send,
107    ) -> impl Future<Output = Result<Browser>> + Send {
108        async move {
109            // Load a browser from a specific executable path
110            let browser_config = BrowserConfig::builder()
111                .viewport(Some(Viewport::web2pdf_viewport()))
112                .chrome_executable(&path)
113                .build()?;
114
115            tracing::debug!("Web2Pdf browser launching using executable path");
116
117            Self::web2pdf_launch_from_config(browser_config).await
118        }
119    }
120
121    /// Closes the browser instance and waits for it to terminate.
122    ///
123    /// # Returns
124    /// A `Result` containing an empty `()` value or an error.
125    fn close_and_wait(mut self) -> impl Future<Output = Result<Browser>> + Send {
126        async move {
127            self.close().await?;
128            self.wait().await?;
129            Ok(self)
130        }
131    }
132
133    /// Create a new browser page
134    ///
135    /// # Arguments
136    /// * `params` - The `CreateTargetParams` to use for creating the page.
137    ///
138    /// # Returns
139    /// A `Result` containing a new `Page` instance or an error.
140    fn web2pdf_new_page(
141        &self,
142        params: impl Into<CreateTargetParams> + Send,
143    ) -> impl Future<Output = Result<Page>> + Send {
144        async move {
145            let page = self.new_page(params).await?;
146            page.emulate_media_type(MediaTypeParams::Print).await?;
147            tracing::debug!("Web2Pdf new page created");
148            Ok(page)
149        }
150    }
151
152    /// Load a cookie file
153    fn web2pdf_load_cookie_file(
154        &self,
155        file: impl AsRef<Path> + Send,
156    ) -> impl Future<Output = Result<()>> + Send {
157        async move {
158            let file_contents = fs::read_to_string(file).await?;
159
160            let cookies = util::parse_cookie_file(&file_contents)?;
161
162            self.set_cookies(cookies).await?;
163            Ok(())
164        }
165    }
166}
167
168impl PageWeb2Pdf for Page {
169    /// Saves the page as a PDF file.
170    ///
171    /// # Arguments
172    /// * `output` - The path to save the PDF file to.
173    ///
174    /// # Returns
175    /// A `Result` containing a `Vec<u8>` containing the PDF data or an error.
176    /// (The Page is already saved as PDF at the specified path)
177    fn web2pdf_save_pdf_standard(
178        &self,
179        output: impl AsRef<Path> + Send,
180    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send {
181        async move {
182            let pdf_params = PrintToPdfParams::builder()
183                .print_background(true)
184                .prefer_css_page_size(true)
185                .build();
186            let pdf = self.save_pdf(pdf_params, output).await?;
187
188            Ok(pdf)
189        }
190    }
191
192    /// Saves the page as a single PDF page
193    ///
194    /// # Note use web2pdf_launch or web2pdf_launch_from_executable_path for correct results
195    /// # Arguments
196    /// * `opts` - The `PrintToPdfParams` to use for saving the PDF.
197    /// * `output` - The path to save the PDF file to.
198    ///
199    /// # Returns
200    /// A `Result` containing a `Vec<u8>` containing the PDF data or an error.
201    /// (The Page is already saved as PDF at the specified path)
202    fn web2pdf_save_pdf_mono(
203        &self,
204        mut opts: PrintToPdfParams,
205        output: impl AsRef<Path> + Send,
206    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send {
207        async move {
208            let layout = self.layout_metrics().await?;
209
210            opts.scale = None;
211            opts.landscape = Some(false);
212
213            // See: https://developer.mozilla.org/en-US/docs/Web/CSS/length#absolute_length_units
214            opts.paper_height = Some(
215                (layout.css_content_size.height / 96.0)
216                    + opts.margin_top.unwrap_or(0.4)
217                    + opts.margin_bottom.unwrap_or(0.4),
218            );
219            opts.paper_width = Some(
220                (layout.css_content_size.width / 96.0)
221                    + opts.margin_left.unwrap_or(0.4)
222                    + opts.margin_right.unwrap_or(0.4),
223            );
224
225            // Some websites force a second (empty) page due to their CSS
226            opts.page_ranges = Some("1".to_string());
227
228            tracing::trace!("Web2Pdf mono page layout: {:?}", layout);
229
230            let pdf = self.save_pdf(opts, output).await?;
231
232            Ok(pdf)
233        }
234    }
235
236    /// Saves the page as a single PDF page
237    ///
238    /// # Note use web2pdf_launch or web2pdf_launch_from_executable_path for correct results
239    /// # Arguments
240    /// * `output` - The path to save the PDF file to.
241    ///
242    /// # Returns
243    /// A `Result` containing a `Vec<u8>` containing the PDF data or an error.
244    /// (The Page is already saved as PDF at the specified path)
245    fn web2pdf_save_pdf_mono_standard(
246        &self,
247        output: impl AsRef<Path> + Send,
248    ) -> impl Future<Output = chromiumoxide::Result<Vec<u8>>> + Send {
249        async move {
250            let opts = PrintToPdfParams::builder()
251                .print_background(true)
252                .prefer_css_page_size(true)
253                .build();
254
255            self.web2pdf_save_pdf_mono(opts, output).await
256        }
257    }
258}
259
260impl ViewportWeb2Pdf for Viewport {
261    // Use standard a4 paper size as page size minus default border (8.268-2*0.4 x 11.693-2*0.4 (inches) * 96 (dpi))
262    // See: https://developer.mozilla.org/en-US/docs/Web/CSS/length#absolute_length_units
263    fn web2pdf_viewport() -> Viewport {
264        Viewport {
265            width: 717,
266            height: 1046,
267            device_scale_factor: Some(1.0),
268            emulating_mobile: false,
269            is_landscape: false,
270            has_touch: false,
271        }
272    }
273}