cdp_html_shot/
browser.rs

1mod temp_dir;
2mod browser_utils;
3mod browser_config;
4mod browser_builder;
5
6use log::error;
7use std::sync::Arc;
8use serde_json::json;
9use std::process::Child;
10use tokio::sync::OnceCell;
11use temp_dir::CustomTempDir;
12use anyhow::{Context, Result};
13use browser_config::BrowserConfig;
14
15use crate::tab::Tab;
16use crate::CaptureOptions;
17use crate::transport::Transport;
18use crate::general_utils::next_id;
19use crate::transport_actor::TransportResponse;
20use crate::browser::browser_builder::BrowserBuilder;
21
22/// The global browser instance.
23static mut BROWSER: OnceCell<Arc<Browser>> = OnceCell::const_new();
24
25#[derive(Debug)]
26struct Process(pub Child, pub CustomTempDir);
27
28/// A browser instance.
29#[derive(Debug)]
30pub struct Browser {
31    transport: Arc<Transport>,
32    process: Process,
33    is_closed: bool,
34}
35
36unsafe impl Send for Browser {}
37unsafe impl Sync for Browser {}
38
39impl Browser {
40    /**
41    Create a new browser instance with default configuration (headless).
42
43    # Example
44    ```no_run
45    use cdp_html_shot::Browser;
46    use anyhow::Result;
47
48    #[tokio::main]
49    async fn main() -> Result<()> {
50        let browser = Browser::new().await?;
51        Ok(())
52    }
53    ```
54    */
55    pub async fn new() -> Result<Self> {
56        BrowserBuilder::new().build().await
57    }
58
59    /// Create a new browser instance with a visible window.
60    pub async fn new_with_head() -> Result<Self> {
61        BrowserBuilder::new()
62            .headless(false)
63            .build()
64            .await
65    }
66
67    /// Create browser instance with custom configuration.
68    async fn create_browser(config: BrowserConfig) -> Result<Self> {
69        let mut child = browser_utils::spawn_chrome_process(&config)?;
70        let ws_url = browser_utils::get_websocket_url(
71            child.stderr.take().context("Failed to get stderr")?
72        ).await?;
73
74        Ok(Self {
75            transport: Arc::new(Transport::new(&ws_url).await?),
76            process: Process(child, config.temp_dir),
77            is_closed: false,
78        })
79    }
80
81    /**
82    Create a new tab.
83
84    # Example
85    ```no_run
86    use cdp_html_shot::Browser;
87    use anyhow::Result;
88
89    #[tokio::main]
90    async fn main() -> Result<()> {
91        let browser = Browser::new().await?;
92        let tab = browser.new_tab().await?;
93        Ok(())
94    }
95    ```
96    */
97    pub async fn new_tab(&self) -> Result<Tab> {
98        Tab::new(self.transport.clone()).await
99    }
100
101    /**
102    Close the initial tab created when the browser starts.
103
104    # Warning
105    Only in headless mode, otherwise it will close the entire browser.
106    */
107    pub async fn close_init_tab(&self) -> Result<()> {
108        let TransportResponse::Response(res) = self.transport.send(json!({
109            "id": next_id(),
110            "method": "Target.getTargets",
111            "params": {}
112        })).await? else { panic!() };
113
114        let target_id = res
115            .result["targetInfos"]
116            .as_array()
117            .unwrap()
118            .iter()
119            .find(|info| {
120                info["type"].as_str().unwrap() == "page"
121            }).unwrap()["targetId"]
122            .as_str()
123            .unwrap();
124
125        self.transport.send(json!({
126            "id": next_id(),
127            "method": "Target.closeTarget",
128            "params": {
129                "targetId": target_id
130            }
131        })).await?;
132
133        Ok(())
134    }
135
136    /**
137    Basic version: Capture a screenshot of an HTML element.
138
139    Returns the base64-encoded image data (JPEG format).
140
141    If you need more control over the capture process, use [`capture_html_with_options`].
142
143    # Arguments
144    - `html`: The HTML content
145    - `selector`: The CSS selector of the element to capture
146
147    [`capture_html_with_options`]: struct.Browser.html#method.capture_html_with_options
148
149    # Example
150    ```no_run
151    use cdp_html_shot::Browser;
152    use anyhow::Result;
153
154    #[tokio::main]
155    async fn main() -> Result<()> {
156        let browser = Browser::new().await?;
157        let base64 = browser.capture_html("<h1>Hello world!</h1>", "h1").await?;
158        Ok(())
159    }
160    ```
161    */
162    pub async fn capture_html(&self, html: &str, selector: &str) -> Result<String> {
163        let tab = self.new_tab().await?;
164
165        tab.set_content(html).await?;
166
167        let element = tab.find_element(selector).await?;
168        let base64 = element.screenshot().await?;
169
170        tab.close().await?;
171        Ok(base64)
172    }
173
174    /**
175    Advanced version: Capture a screenshot of an HTML element with additional options.
176
177    # Arguments
178    - `html`: The HTML content
179    - `selector`: The CSS selector of the element to capture
180    - `options`: Configuration options for the capture
181
182    # Example
183    ```no_run
184    use cdp_html_shot::{Browser, CaptureOptions};
185    use anyhow::Result;
186
187    #[tokio::main]
188    async fn main() -> Result<()> {
189        let browser = Browser::new().await?;
190        let options = CaptureOptions::new()
191            .with_raw_png(true);
192
193        let base64 = browser
194            .capture_html_with_options(
195                "<h1>Hello world!</h1>",
196                "h1",
197                options
198            ).await?;
199        Ok(())
200    }
201    ```
202    */
203    pub async fn capture_html_with_options(
204        &self,
205        html: &str,
206        selector: &str,
207        options: CaptureOptions,
208    ) -> Result<String> {
209        let tab = self.new_tab().await?;
210
211        tab.set_content(html).await?;
212
213        let element = tab.find_element(selector).await?;
214
215        let base64 = if options.raw_png {
216            element.raw_screenshot().await?
217        } else {
218            element.screenshot().await?
219        };
220
221        tab.close().await?;
222
223        Ok(base64)
224    }
225
226    /**
227    Close the browser.
228
229    This will kill the browser process and clean up temporary files.
230
231    Normally, this method does not need to be called manually,
232    because it will be called automatically when the `Browser` instance is destroyed.
233
234    # Example
235    ```no_run
236    use cdp_html_shot::Browser;
237    use anyhow::Result;
238
239    #[tokio::main]
240    async fn main() -> Result<()> {
241        let mut browser = Browser::new().await?;
242        browser.close()?;
243        Ok(())
244    }
245    ```
246    */
247    pub fn close(&mut self) -> Result<()> {
248        if self.is_closed {
249            return Ok(());
250        }
251
252        Arc::get_mut(&mut self.transport)
253            .unwrap()
254            .shutdown()?;
255
256        self.process.0
257            .kill()
258            .and_then(|_| self.process.0.wait())
259            .context("Failed to kill the browser process")?;
260
261        self.process.1
262            .cleanup()?;
263
264        self.is_closed = true;
265        Ok(())
266    }
267}
268
269impl Browser {
270    /**
271    Get the global Browser instance.
272
273    Creates a new one if it doesn't exist.
274
275    This method is thread-safe and ensures only one browser instance is created.
276
277    The browser will be automatically closed
278    when all references are dropped or when the program exits.
279
280    # Example
281    ```no_run
282    use cdp_html_shot::Browser;
283    use anyhow::Result;
284
285    #[tokio::main]
286    async fn main() -> Result<()> {
287        let browser = Browser::instance().await;
288        let tab = browser.new_tab().await?;
289
290        Browser::close_instance();
291        Ok(())
292    }
293    ```
294    */
295    pub async fn instance() -> Arc<Browser> {
296        unsafe {
297            let browser = BROWSER
298                .get_or_init(|| async {
299                    let browser = Browser::new().await.unwrap();
300                    browser.close_init_tab().await.unwrap();
301                    Arc::new(browser)
302                })
303                .await;
304
305            browser.clone()
306        }
307    }
308
309    /**
310    Close the global Browser instance.
311
312    Please ensure that this method is called before the program exits,
313    and there should be no Browser instances in use at this time.
314    */
315    pub fn close_instance() -> Option<()> {
316        unsafe {
317            Arc::get_mut(&mut BROWSER.take()?)?.close().ok()
318        }
319    }
320}
321
322impl Drop for Browser {
323    fn drop(&mut self) {
324        if !self.is_closed {
325            if let Err(e) = self.close() {
326                error!("Error closing browser: {:?}", e);
327            }
328        }
329    }
330}