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