capmonstercloud_client/requests/
image_to_text.rs1use base64::{engine::general_purpose::STANDARD, Engine as _};
2use serde::{Serialize, Serializer};
3
4use super::CaptchaRequest;
5use crate::error::{Error, Result};
6use crate::timeouts::GetResultTimeouts;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TextModule {
13 Amazon,
14 Apple,
15 Botdetect,
16 Facebook,
17 Gmx,
18 Google,
19 Hotmail,
20 Mailru,
21 Okeng,
22 Okrus,
23 Partiallyblur,
24 Ramblerrus,
25 Ramblerrusnew,
26 Solvemedia,
27 Steam,
28 Vk,
29 Whatsapp,
30 WikimediaPaddleEnsemble,
31 Yandex,
32 Yandexnew,
34 Yandexwave,
35 Yandexwavelatin,
37 CaptchaMath,
38 Sat,
39 BlsText,
40 Universal,
42 Custom(String),
44}
45
46impl TextModule {
47 pub fn as_str(&self) -> &str {
49 match self {
50 TextModule::Amazon => "amazon",
51 TextModule::Apple => "apple",
52 TextModule::Botdetect => "botdetect",
53 TextModule::Facebook => "facebook",
54 TextModule::Gmx => "gmx",
55 TextModule::Google => "google",
56 TextModule::Hotmail => "hotmail",
57 TextModule::Mailru => "mailru",
58 TextModule::Okeng => "okeng",
59 TextModule::Okrus => "okrus",
60 TextModule::Partiallyblur => "partiallyblur",
61 TextModule::Ramblerrus => "ramblerrus",
62 TextModule::Ramblerrusnew => "ramblerrusnew",
63 TextModule::Solvemedia => "solvemedia",
64 TextModule::Steam => "steam",
65 TextModule::Vk => "vk",
66 TextModule::Whatsapp => "whatsapp",
67 TextModule::WikimediaPaddleEnsemble => "wikimedia_paddle_ensemble",
68 TextModule::Yandex => "yandex",
69 TextModule::Yandexnew => "yandexnew",
70 TextModule::Yandexwave => "yandexwave",
71 TextModule::Yandexwavelatin => "yandexwavelatin",
72 TextModule::CaptchaMath => "captcha_math",
73 TextModule::Sat => "sat",
74 TextModule::BlsText => "bls_text",
75 TextModule::Universal => "universal",
76 TextModule::Custom(name) => name,
77 }
78 }
79}
80
81impl Serialize for TextModule {
82 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
83 s.serialize_str(self.as_str())
84 }
85}
86
87fn as_base64<S: Serializer>(bytes: &[u8], s: S) -> std::result::Result<S::Ok, S::Error> {
88 s.serialize_str(&STANDARD.encode(bytes))
89}
90
91#[derive(Debug, Clone, Serialize)]
93pub struct ImageToTextRequest {
94 #[serde(rename = "body", serialize_with = "as_base64")]
96 pub image_bytes: Vec<u8>,
97 #[serde(rename = "capMonsterModule", skip_serializing_if = "Option::is_none")]
98 pub module_name: Option<TextModule>,
99 #[serde(
101 rename = "recognizingThreshold",
102 skip_serializing_if = "Option::is_none"
103 )]
104 pub threshold: Option<u8>,
105 #[serde(rename = "case", skip_serializing_if = "Option::is_none")]
107 pub case: Option<bool>,
108 #[serde(
110 serialize_with = "bool_as_int",
111 skip_serializing_if = "Option::is_none"
112 )]
113 pub numeric: Option<bool>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub math: Option<bool>,
117}
118
119fn bool_as_int<S: Serializer>(v: &Option<bool>, s: S) -> std::result::Result<S::Ok, S::Error> {
120 match v {
121 Some(b) => s.serialize_u8(*b as u8),
122 None => s.serialize_none(),
123 }
124}
125
126impl ImageToTextRequest {
127 pub fn new(image_bytes: impl Into<Vec<u8>>) -> Self {
128 Self {
129 image_bytes: image_bytes.into(),
130 module_name: None,
131 threshold: None,
132 case: None,
133 numeric: None,
134 math: None,
135 }
136 }
137
138 pub fn with_module_name(mut self, value: TextModule) -> Self {
139 self.module_name = Some(value);
140 self
141 }
142
143 pub fn with_threshold(mut self, value: u8) -> Self {
144 self.threshold = Some(value);
145 self
146 }
147
148 pub fn with_case(mut self, value: bool) -> Self {
149 self.case = Some(value);
150 self
151 }
152
153 pub fn with_numeric(mut self, value: bool) -> Self {
154 self.numeric = Some(value);
155 self
156 }
157
158 pub fn with_math(mut self, value: bool) -> Self {
159 self.math = Some(value);
160 self
161 }
162}
163
164impl CaptchaRequest for ImageToTextRequest {
165 const TASK_TYPE: &'static str = "ImageToTextTask";
166
167 fn timeouts(&self) -> GetResultTimeouts {
168 GetResultTimeouts::secs(0.35, 0.0, 0.2, 10.0)
169 }
170
171 fn validate(&self) -> Result<()> {
172 if self.image_bytes.is_empty() {
173 return Err(Error::Validation("body must not be empty".into()));
174 }
175 if matches!(self.threshold, Some(t) if t > 100) {
176 return Err(Error::Validation(
177 "threshold must be between 0 and 100".into(),
178 ));
179 }
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use serde_json::json;
188
189 #[test]
190 fn body_is_base64_and_numeric_is_int() {
191 let task = ImageToTextRequest::new(b"abc".to_vec())
192 .with_module_name(TextModule::Yandexnew)
193 .with_threshold(90)
194 .with_numeric(true)
195 .to_task()
196 .unwrap();
197 assert_eq!(
198 task,
199 json!({"type": "ImageToTextTask", "body": "YWJj", "capMonsterModule": "yandexnew",
200 "recognizingThreshold": 90, "numeric": 1})
201 );
202 }
203
204 #[test]
205 fn threshold_out_of_range_is_rejected() {
206 let err = ImageToTextRequest::new(vec![1])
207 .with_threshold(101)
208 .to_task()
209 .unwrap_err();
210 assert!(matches!(err, Error::Validation(_)));
211 }
212
213 #[test]
214 fn empty_image_bytes_are_rejected() {
215 let err = ImageToTextRequest::new(Vec::new()).to_task().unwrap_err();
216 assert!(matches!(err, Error::Validation(ref m) if m.contains("body")));
217 }
218}