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}