1use reqwest::Client;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::time::Duration;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum CaptureError {
9 #[error("HTTP request failed: {0}")]
10 HttpError(#[from] reqwest::Error),
11 #[error("URL parsing failed: {0}")]
12 UrlError(#[from] url::ParseError),
13 #[error("Key and Secret are required")]
14 MissingCredentials,
15 #[error("URL is required")]
16 MissingUrl,
17 #[error("URL should be a string")]
18 InvalidUrl,
19}
20
21pub type Result<T> = std::result::Result<T, CaptureError>;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum RequestType {
25 Image,
26 Pdf,
27 Content,
28 Metadata,
29}
30
31impl RequestType {
32 fn as_str(&self) -> &'static str {
33 match self {
34 RequestType::Image => "image",
35 RequestType::Pdf => "pdf",
36 RequestType::Content => "content",
37 RequestType::Metadata => "metadata",
38 }
39 }
40}
41
42pub type RequestOptions = HashMap<String, serde_json::Value>;
43
44#[derive(Debug, Clone, Default)]
45pub struct ScreenshotOptions {
46 pub vw: Option<u32>,
48 pub vh: Option<u32>,
49 pub scale_factor: Option<f64>,
50
51 pub full: Option<bool>,
53 pub delay: Option<u32>,
54 pub wait_for: Option<String>,
55 pub wait_for_id: Option<String>,
56
57 pub dark_mode: Option<bool>,
59 pub transparent: Option<bool>,
60 pub selector: Option<String>,
61 pub selector_id: Option<String>,
62
63 pub block_cookie_banners: Option<bool>,
65 pub block_ads: Option<bool>,
66 pub bypass_bot_detection: Option<bool>,
67
68 pub image_type: Option<String>,
70 pub best_format: Option<bool>,
71 pub resize_width: Option<u32>,
72 pub resize_height: Option<u32>,
73
74 pub http_auth: Option<String>,
76 pub user_agent: Option<String>,
77 pub fresh: Option<bool>,
78
79 pub additional_options: Option<RequestOptions>,
81}
82
83#[derive(Debug, Clone, Default)]
84pub struct PdfOptions {
85 pub http_auth: Option<String>,
87 pub user_agent: Option<String>,
88
89 pub width: Option<String>,
91 pub height: Option<String>,
92 pub format: Option<String>,
93
94 pub margin_top: Option<String>,
96 pub margin_right: Option<String>,
97 pub margin_bottom: Option<String>,
98 pub margin_left: Option<String>,
99
100 pub scale: Option<f64>,
102 pub landscape: Option<bool>,
103 pub delay: Option<u32>,
104
105 pub file_name: Option<String>,
107 pub s3_acl: Option<String>,
108 pub s3_redirect: Option<bool>,
109 pub timestamp: Option<bool>,
110
111 pub additional_options: Option<RequestOptions>,
113}
114
115#[derive(Debug, Clone, Default)]
116pub struct ContentOptions {
117 pub http_auth: Option<String>,
118 pub user_agent: Option<String>,
119 pub delay: Option<u32>,
120 pub wait_for: Option<String>,
121 pub wait_for_id: Option<String>,
122
123 pub additional_options: Option<RequestOptions>,
125}
126
127#[derive(Debug, Clone, Default)]
128pub struct MetadataOptions {
129 pub additional_options: Option<RequestOptions>,
131}
132
133impl ScreenshotOptions {
134 pub fn to_request_options(&self) -> RequestOptions {
135 let mut options = RequestOptions::new();
136
137 if let Some(vw) = self.vw {
138 options.insert("vw".to_string(), serde_json::Value::Number(vw.into()));
139 }
140 if let Some(vh) = self.vh {
141 options.insert("vh".to_string(), serde_json::Value::Number(vh.into()));
142 }
143 if let Some(scale_factor) = self.scale_factor {
144 if let Some(num) = serde_json::Number::from_f64(scale_factor) {
145 options.insert("scaleFactor".to_string(), serde_json::Value::Number(num));
146 }
147 }
148 if let Some(full) = self.full {
149 options.insert("full".to_string(), serde_json::Value::Bool(full));
150 }
151 if let Some(delay) = self.delay {
152 options.insert("delay".to_string(), serde_json::Value::Number(delay.into()));
153 }
154 if let Some(wait_for) = &self.wait_for {
155 options.insert(
156 "waitFor".to_string(),
157 serde_json::Value::String(wait_for.clone()),
158 );
159 }
160 if let Some(wait_for_id) = &self.wait_for_id {
161 options.insert(
162 "waitForId".to_string(),
163 serde_json::Value::String(wait_for_id.clone()),
164 );
165 }
166 if let Some(dark_mode) = self.dark_mode {
167 options.insert("darkMode".to_string(), serde_json::Value::Bool(dark_mode));
168 }
169 if let Some(transparent) = self.transparent {
170 options.insert(
171 "transparent".to_string(),
172 serde_json::Value::Bool(transparent),
173 );
174 }
175 if let Some(selector) = &self.selector {
176 options.insert(
177 "selector".to_string(),
178 serde_json::Value::String(selector.clone()),
179 );
180 }
181 if let Some(selector_id) = &self.selector_id {
182 options.insert(
183 "selectorId".to_string(),
184 serde_json::Value::String(selector_id.clone()),
185 );
186 }
187 if let Some(block_cookie_banners) = self.block_cookie_banners {
188 options.insert(
189 "blockCookieBanners".to_string(),
190 serde_json::Value::Bool(block_cookie_banners),
191 );
192 }
193 if let Some(block_ads) = self.block_ads {
194 options.insert("blockAds".to_string(), serde_json::Value::Bool(block_ads));
195 }
196 if let Some(bypass_bot_detection) = self.bypass_bot_detection {
197 options.insert(
198 "bypassBotDetection".to_string(),
199 serde_json::Value::Bool(bypass_bot_detection),
200 );
201 }
202 if let Some(image_type) = &self.image_type {
203 options.insert(
204 "type".to_string(),
205 serde_json::Value::String(image_type.clone()),
206 );
207 }
208 if let Some(best_format) = self.best_format {
209 options.insert(
210 "bestFormat".to_string(),
211 serde_json::Value::Bool(best_format),
212 );
213 }
214 if let Some(resize_width) = self.resize_width {
215 options.insert(
216 "resizeWidth".to_string(),
217 serde_json::Value::Number(resize_width.into()),
218 );
219 }
220 if let Some(resize_height) = self.resize_height {
221 options.insert(
222 "resizeHeight".to_string(),
223 serde_json::Value::Number(resize_height.into()),
224 );
225 }
226 if let Some(http_auth) = &self.http_auth {
227 options.insert(
228 "httpAuth".to_string(),
229 serde_json::Value::String(http_auth.clone()),
230 );
231 }
232 if let Some(user_agent) = &self.user_agent {
233 options.insert(
234 "userAgent".to_string(),
235 serde_json::Value::String(user_agent.clone()),
236 );
237 }
238 if let Some(fresh) = self.fresh {
239 options.insert("fresh".to_string(), serde_json::Value::Bool(fresh));
240 }
241
242 if let Some(additional) = &self.additional_options {
244 for (key, value) in additional {
245 options.insert(key.clone(), value.clone());
246 }
247 }
248
249 options
250 }
251}
252
253impl PdfOptions {
254 pub fn to_request_options(&self) -> RequestOptions {
255 let mut options = RequestOptions::new();
256
257 if let Some(http_auth) = &self.http_auth {
258 options.insert(
259 "httpAuth".to_string(),
260 serde_json::Value::String(http_auth.clone()),
261 );
262 }
263 if let Some(user_agent) = &self.user_agent {
264 options.insert(
265 "userAgent".to_string(),
266 serde_json::Value::String(user_agent.clone()),
267 );
268 }
269 if let Some(width) = &self.width {
270 options.insert(
271 "width".to_string(),
272 serde_json::Value::String(width.clone()),
273 );
274 }
275 if let Some(height) = &self.height {
276 options.insert(
277 "height".to_string(),
278 serde_json::Value::String(height.clone()),
279 );
280 }
281 if let Some(format) = &self.format {
282 options.insert(
283 "format".to_string(),
284 serde_json::Value::String(format.clone()),
285 );
286 }
287 if let Some(margin_top) = &self.margin_top {
288 options.insert(
289 "marginTop".to_string(),
290 serde_json::Value::String(margin_top.clone()),
291 );
292 }
293 if let Some(margin_right) = &self.margin_right {
294 options.insert(
295 "marginRight".to_string(),
296 serde_json::Value::String(margin_right.clone()),
297 );
298 }
299 if let Some(margin_bottom) = &self.margin_bottom {
300 options.insert(
301 "marginBottom".to_string(),
302 serde_json::Value::String(margin_bottom.clone()),
303 );
304 }
305 if let Some(margin_left) = &self.margin_left {
306 options.insert(
307 "marginLeft".to_string(),
308 serde_json::Value::String(margin_left.clone()),
309 );
310 }
311 if let Some(scale) = self.scale {
312 if let Some(num) = serde_json::Number::from_f64(scale) {
313 options.insert("scale".to_string(), serde_json::Value::Number(num));
314 }
315 }
316 if let Some(landscape) = self.landscape {
317 options.insert("landscape".to_string(), serde_json::Value::Bool(landscape));
318 }
319 if let Some(delay) = self.delay {
320 options.insert("delay".to_string(), serde_json::Value::Number(delay.into()));
321 }
322 if let Some(file_name) = &self.file_name {
323 options.insert(
324 "fileName".to_string(),
325 serde_json::Value::String(file_name.clone()),
326 );
327 }
328 if let Some(s3_acl) = &self.s3_acl {
329 options.insert(
330 "s3Acl".to_string(),
331 serde_json::Value::String(s3_acl.clone()),
332 );
333 }
334 if let Some(s3_redirect) = self.s3_redirect {
335 options.insert(
336 "s3Redirect".to_string(),
337 serde_json::Value::Bool(s3_redirect),
338 );
339 }
340 if let Some(timestamp) = self.timestamp {
341 options.insert("timestamp".to_string(), serde_json::Value::Bool(timestamp));
342 }
343
344 if let Some(additional) = &self.additional_options {
346 for (key, value) in additional {
347 options.insert(key.clone(), value.clone());
348 }
349 }
350
351 options
352 }
353}
354
355impl ContentOptions {
356 pub fn to_request_options(&self) -> RequestOptions {
357 let mut options = RequestOptions::new();
358
359 if let Some(http_auth) = &self.http_auth {
360 options.insert(
361 "httpAuth".to_string(),
362 serde_json::Value::String(http_auth.clone()),
363 );
364 }
365 if let Some(user_agent) = &self.user_agent {
366 options.insert(
367 "userAgent".to_string(),
368 serde_json::Value::String(user_agent.clone()),
369 );
370 }
371 if let Some(delay) = self.delay {
372 options.insert("delay".to_string(), serde_json::Value::Number(delay.into()));
373 }
374 if let Some(wait_for) = &self.wait_for {
375 options.insert(
376 "waitFor".to_string(),
377 serde_json::Value::String(wait_for.clone()),
378 );
379 }
380 if let Some(wait_for_id) = &self.wait_for_id {
381 options.insert(
382 "waitForId".to_string(),
383 serde_json::Value::String(wait_for_id.clone()),
384 );
385 }
386
387 if let Some(additional) = &self.additional_options {
389 for (key, value) in additional {
390 options.insert(key.clone(), value.clone());
391 }
392 }
393
394 options
395 }
396}
397
398impl MetadataOptions {
399 pub fn to_request_options(&self) -> RequestOptions {
400 let mut options = RequestOptions::new();
401
402 if let Some(additional) = &self.additional_options {
404 for (key, value) in additional {
405 options.insert(key.clone(), value.clone());
406 }
407 }
408
409 options
410 }
411}
412
413#[derive(Debug, Clone, Default)]
414pub struct CaptureOptions {
415 pub use_edge: bool,
416 pub timeout: Option<Duration>,
417 pub client: Option<Client>,
418}
419
420impl CaptureOptions {
421 pub fn new() -> Self {
422 Self::default()
423 }
424
425 pub fn with_edge(mut self) -> Self {
426 self.use_edge = true;
427 self
428 }
429
430 pub fn with_timeout(mut self, timeout: Duration) -> Self {
431 self.timeout = Some(timeout);
432 self
433 }
434
435 pub fn with_client(mut self, client: Client) -> Self {
436 self.client = Some(client);
437 self
438 }
439}
440
441#[derive(Debug, Deserialize)]
442pub struct ContentResponse {
443 pub success: bool,
444 pub html: String,
445 #[serde(rename = "textContent")]
446 pub text_content: String,
447}
448
449#[derive(Debug, Deserialize)]
450pub struct MetadataResponse {
451 pub success: bool,
452 pub metadata: HashMap<String, serde_json::Value>,
453}
454
455pub struct Capture {
456 key: String,
457 secret: String,
458 options: CaptureOptions,
459 client: Client,
460}
461
462impl Capture {
463 const API_URL: &'static str = "https://cdn.capture.page";
464 const EDGE_URL: &'static str = "https://edge.capture.page";
465
466 pub fn new(key: String, secret: String) -> Self {
467 let options = CaptureOptions::default();
468 let client = options.client.clone().unwrap_or_else(|| {
469 let mut builder = Client::builder();
470 if let Some(timeout) = options.timeout {
471 builder = builder.timeout(timeout);
472 }
473 builder.build().unwrap_or_else(|_| Client::new())
474 });
475
476 Self {
477 key,
478 secret,
479 options,
480 client,
481 }
482 }
483
484 pub fn with_options(key: String, secret: String, options: CaptureOptions) -> Self {
485 let client = options.client.clone().unwrap_or_else(|| {
486 let mut builder = Client::builder();
487 if let Some(timeout) = options.timeout {
488 builder = builder.timeout(timeout);
489 }
490 builder.build().unwrap_or_else(|_| Client::new())
491 });
492
493 Self {
494 key,
495 secret,
496 options,
497 client,
498 }
499 }
500
501 pub fn with_edge(mut self) -> Self {
502 self.options.use_edge = true;
503 self
504 }
505
506 pub fn with_timeout(mut self, timeout: Duration) -> Self {
507 self.options.timeout = Some(timeout);
508 let builder = Client::builder().timeout(timeout);
510 self.client = builder.build().unwrap_or_else(|_| Client::new());
511 self
512 }
513
514 pub fn with_client(mut self, client: Client) -> Self {
515 self.client = client;
516 self.options.client = Some(self.client.clone());
517 self
518 }
519
520 fn generate_token(&self, secret: &str, url: &str) -> String {
521 format!("{:x}", md5::compute(format!("{secret}{url}")))
522 }
523
524 fn to_query_string(&self, options: &RequestOptions) -> String {
525 let mut params = Vec::new();
526
527 for (key, value) in options {
528 if key == "format" {
529 continue;
530 }
531
532 let value_str = match value {
533 serde_json::Value::String(s) => s.clone(),
534 serde_json::Value::Number(n) => n.to_string(),
535 serde_json::Value::Bool(b) => b.to_string(),
536 _ => continue,
537 };
538
539 if !value_str.is_empty() {
540 params.push(format!(
541 "{}={}",
542 urlencoding::encode(key),
543 urlencoding::encode(&value_str)
544 ));
545 }
546 }
547
548 params.join("&")
549 }
550
551 fn build_url(
552 &self,
553 request_type: RequestType,
554 url: &str,
555 request_options: Option<&RequestOptions>,
556 ) -> Result<String> {
557 if self.key.is_empty() || self.secret.is_empty() {
558 return Err(CaptureError::MissingCredentials);
559 }
560
561 if url.is_empty() {
562 return Err(CaptureError::MissingUrl);
563 }
564
565 let mut options = request_options.cloned().unwrap_or_default();
566 options.insert(
567 "url".to_string(),
568 serde_json::Value::String(url.to_string()),
569 );
570
571 let query = self.to_query_string(&options);
572 let token = self.generate_token(&self.secret, &query);
573
574 let base_url = if self.options.use_edge {
575 Self::EDGE_URL
576 } else {
577 Self::API_URL
578 };
579
580 Ok(format!(
581 "{}/{}/{}/{}?{}",
582 base_url,
583 self.key,
584 token,
585 request_type.as_str(),
586 query
587 ))
588 }
589
590 pub fn build_image_url(&self, url: &str, options: Option<&RequestOptions>) -> Result<String> {
591 self.build_url(RequestType::Image, url, options)
592 }
593
594 pub fn build_pdf_url(&self, url: &str, options: Option<&RequestOptions>) -> Result<String> {
595 self.build_url(RequestType::Pdf, url, options)
596 }
597
598 pub fn build_content_url(&self, url: &str, options: Option<&RequestOptions>) -> Result<String> {
599 self.build_url(RequestType::Content, url, options)
600 }
601
602 pub fn build_metadata_url(
603 &self,
604 url: &str,
605 options: Option<&RequestOptions>,
606 ) -> Result<String> {
607 self.build_url(RequestType::Metadata, url, options)
608 }
609
610 pub fn build_screenshot_url(
612 &self,
613 url: &str,
614 options: Option<&ScreenshotOptions>,
615 ) -> Result<String> {
616 let request_options = options.map(|o| o.to_request_options());
617 self.build_url(RequestType::Image, url, request_options.as_ref())
618 }
619
620 pub fn build_pdf_url_structured(
621 &self,
622 url: &str,
623 options: Option<&PdfOptions>,
624 ) -> Result<String> {
625 let request_options = options.map(|o| o.to_request_options());
626 self.build_url(RequestType::Pdf, url, request_options.as_ref())
627 }
628
629 pub fn build_content_url_structured(
630 &self,
631 url: &str,
632 options: Option<&ContentOptions>,
633 ) -> Result<String> {
634 let request_options = options.map(|o| o.to_request_options());
635 self.build_url(RequestType::Content, url, request_options.as_ref())
636 }
637
638 pub fn build_metadata_url_structured(
639 &self,
640 url: &str,
641 options: Option<&MetadataOptions>,
642 ) -> Result<String> {
643 let request_options = options.map(|o| o.to_request_options());
644 self.build_url(RequestType::Metadata, url, request_options.as_ref())
645 }
646
647 pub async fn fetch_image(
648 &self,
649 url: &str,
650 options: Option<&RequestOptions>,
651 ) -> Result<Vec<u8>> {
652 let capture_url = self.build_image_url(url, options)?;
653 let response = self.client.get(&capture_url).send().await?;
654 let bytes = response.bytes().await?;
655 Ok(bytes.to_vec())
656 }
657
658 pub async fn fetch_pdf(&self, url: &str, options: Option<&RequestOptions>) -> Result<Vec<u8>> {
659 let capture_url = self.build_pdf_url(url, options)?;
660 let response = self.client.get(&capture_url).send().await?;
661 let bytes = response.bytes().await?;
662 Ok(bytes.to_vec())
663 }
664
665 pub async fn fetch_content(
666 &self,
667 url: &str,
668 options: Option<&RequestOptions>,
669 ) -> Result<ContentResponse> {
670 let capture_url = self.build_content_url(url, options)?;
671 let response = self.client.get(&capture_url).send().await?;
672 let content = response.json::<ContentResponse>().await?;
673 Ok(content)
674 }
675
676 pub async fn fetch_metadata(
677 &self,
678 url: &str,
679 options: Option<&RequestOptions>,
680 ) -> Result<MetadataResponse> {
681 let capture_url = self.build_metadata_url(url, options)?;
682 let response = self.client.get(&capture_url).send().await?;
683 let metadata = response.json::<MetadataResponse>().await?;
684 Ok(metadata)
685 }
686
687 pub async fn fetch_screenshot(
689 &self,
690 url: &str,
691 options: Option<&ScreenshotOptions>,
692 ) -> Result<Vec<u8>> {
693 let capture_url = self.build_screenshot_url(url, options)?;
694 let response = self.client.get(&capture_url).send().await?;
695 let bytes = response.bytes().await?;
696 Ok(bytes.to_vec())
697 }
698
699 pub async fn fetch_pdf_structured(
700 &self,
701 url: &str,
702 options: Option<&PdfOptions>,
703 ) -> Result<Vec<u8>> {
704 let capture_url = self.build_pdf_url_structured(url, options)?;
705 let response = self.client.get(&capture_url).send().await?;
706 let bytes = response.bytes().await?;
707 Ok(bytes.to_vec())
708 }
709
710 pub async fn fetch_content_structured(
711 &self,
712 url: &str,
713 options: Option<&ContentOptions>,
714 ) -> Result<ContentResponse> {
715 let capture_url = self.build_content_url_structured(url, options)?;
716 let response = self.client.get(&capture_url).send().await?;
717 let content = response.json::<ContentResponse>().await?;
718 Ok(content)
719 }
720
721 pub async fn fetch_metadata_structured(
722 &self,
723 url: &str,
724 options: Option<&MetadataOptions>,
725 ) -> Result<MetadataResponse> {
726 let capture_url = self.build_metadata_url_structured(url, options)?;
727 let response = self.client.get(&capture_url).send().await?;
728 let metadata = response.json::<MetadataResponse>().await?;
729 Ok(metadata)
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn test_capture_new() {
739 let capture = Capture::new("test_key".to_string(), "test_secret".to_string());
740 assert_eq!(capture.key, "test_key");
741 assert_eq!(capture.secret, "test_secret");
742 assert!(!capture.options.use_edge);
743 }
744
745 #[test]
746 fn test_capture_with_edge() {
747 let options = CaptureOptions::new().with_edge();
748 let capture =
749 Capture::with_options("test_key".to_string(), "test_secret".to_string(), options);
750 assert!(capture.options.use_edge);
751 }
752
753 #[test]
754 fn test_build_image_url() {
755 let capture = Capture::new("test_key".to_string(), "test_secret".to_string());
756 let url = capture
757 .build_image_url("https://example.com", None)
758 .unwrap();
759 assert!(url.contains("test_key"));
760 assert!(url.contains("image"));
761 assert!(url.contains("https://cdn.capture.page"));
762 }
763
764 #[test]
765 fn test_build_image_url_with_edge() {
766 let options = CaptureOptions::new().with_edge();
767 let capture =
768 Capture::with_options("test_key".to_string(), "test_secret".to_string(), options);
769 let url = capture
770 .build_image_url("https://example.com", None)
771 .unwrap();
772 assert!(url.contains("https://edge.capture.page"));
773 }
774
775 #[test]
776 fn test_missing_credentials() {
777 let capture = Capture::new("".to_string(), "".to_string());
778 let result = capture.build_image_url("https://example.com", None);
779 assert!(matches!(result, Err(CaptureError::MissingCredentials)));
780 }
781
782 #[test]
783 fn test_missing_url() {
784 let capture = Capture::new("test_key".to_string(), "test_secret".to_string());
785 let result = capture.build_image_url("", None);
786 assert!(matches!(result, Err(CaptureError::MissingUrl)));
787 }
788}