1#![doc(html_root_url = "https://docs.rs/captcha-rs/latest")]
2
3use image::DynamicImage;
22use imageproc::noise::{gaussian_noise_mut, salt_and_pepper_noise_mut};
23use rand::{rng, Rng};
24
25use crate::captcha::{
26 cyclic_write_character, draw_interference_ellipse, draw_interference_line, get_image,
27 to_base64_str,
28};
29
30mod captcha;
31
32pub struct Captcha {
33 pub text: String,
34 pub image: DynamicImage,
35 pub compression: u8,
36 pub dark_mode: bool,
37}
38
39impl Captcha {
40 pub fn to_base64(&self) -> String {
41 to_base64_str(&self.image, self.compression)
42 }
43}
44
45#[derive(Default)]
46pub struct CaptchaBuilder {
47 text: Option<String>,
48 length: usize,
49 characters: Vec<char>,
50 width: u32,
51 height: u32,
52 dark_mode: bool,
53 complexity: u32,
54 compression: u8,
55 drop_shadow: bool,
56 interference_lines: usize,
57 interference_ellipses: usize,
58 distortion: u32,
59}
60
61impl CaptchaBuilder {
62 pub fn new() -> Self {
63 CaptchaBuilder {
64 text: None,
65 length: 5,
66 characters: captcha::BASIC_CHAR.to_vec(),
67 width: 130,
68 height: 40,
69 dark_mode: false,
70 complexity: 1,
71 compression: 40,
72 drop_shadow: false,
73 interference_lines: 2,
74 interference_ellipses: 2,
75 distortion: 0,
76 }
77 }
78
79 pub fn text(mut self, text: String) -> Self {
80 self.text = Some(text);
81 self
82 }
83
84 pub fn length(mut self, length: usize) -> Self {
85 self.length = length.max(1);
86 self
87 }
88
89 pub fn chars(mut self, chars: Vec<char>) -> Self {
90 self.characters = chars;
91 self
92 }
93
94 pub fn width(mut self, width: u32) -> Self {
95 self.width = width.clamp(30, 2000);
96 self
97 }
98
99 pub fn height(mut self, height: u32) -> Self {
100 self.height = height.clamp(20, 2000);
101 self
102 }
103
104 pub fn dark_mode(mut self, dark_mode: bool) -> Self {
105 self.dark_mode = dark_mode;
106 self
107 }
108
109 pub fn complexity(mut self, complexity: u32) -> Self {
110 self.complexity = complexity.clamp(1, 10);
111 self
112 }
113
114 pub fn compression(mut self, compression: u8) -> Self {
115 self.compression = compression.clamp(1, 99);
116 self
117 }
118
119 pub fn drop_shadow(mut self, drop_shadow: bool) -> Self {
120 self.drop_shadow = drop_shadow;
121 self
122 }
123
124 pub fn interference_lines(mut self, lines: usize) -> Self {
125 self.interference_lines = lines;
126 self
127 }
128
129 pub fn interference_ellipses(mut self, ellipses: usize) -> Self {
130 self.interference_ellipses = ellipses;
131 self
132 }
133
134 pub fn distortion(mut self, distortion: u32) -> Self {
135 self.distortion = distortion;
136 self
137 }
138
139 pub fn build(self) -> Captcha {
140 let text = match self.text {
141 Some(t) if !t.is_empty() => t,
142 _ => captcha::get_captcha(self.length, &self.characters).join(""),
143 };
144
145 let mut image = get_image(self.width, self.height, self.dark_mode);
147
148 let res: Vec<String> = text.chars().map(|x| x.to_string()).collect();
149
150 cyclic_write_character(&res, &mut image, self.dark_mode, self.drop_shadow);
152
153 if self.distortion > 0 {
154 captcha::apply_wavy_distortion(&mut image, self.distortion);
155 }
156
157 for _ in 0..self.interference_lines {
159 draw_interference_line(&mut image, self.dark_mode);
160 }
161
162 draw_interference_ellipse(self.interference_ellipses, &mut image, self.dark_mode);
164
165 if self.complexity > 1 {
166 let mut rng = rng();
167
168 gaussian_noise_mut(
169 &mut image,
170 (self.complexity - 1) as f64,
171 ((5 * self.complexity) - 5) as f64,
172 rng.random::<u64>(),
173 );
174
175 salt_and_pepper_noise_mut(
176 &mut image,
177 (0.002 * self.complexity as f64) - 0.002,
178 rng.random::<u64>(),
179 );
180 }
181
182 Captcha {
183 text,
184 image: DynamicImage::ImageRgb8(image),
185 compression: self.compression,
186 dark_mode: self.dark_mode,
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use crate::CaptchaBuilder;
194
195 #[test]
196 fn it_generates_a_captcha() {
197 let _dark_mode = false;
198 let _text_length = 5;
199 let _width = 130;
200 let _height = 40;
201
202 let start = std::time::Instant::now();
203
204 let captcha = CaptchaBuilder::new()
205 .text(String::from("based"))
206 .width(200)
207 .height(70)
208 .dark_mode(false)
209 .build();
210
211 let duration = start.elapsed();
212 println!("Time elapsed in generating captcha() is: {:?}", duration);
213
214 assert_eq!(captcha.text.len(), 5);
215 let base_img = captcha.to_base64();
216 assert!(base_img.starts_with("data:image/jpeg;base64,"));
217 println!("text: {}", captcha.text);
218 println!("base_img: {}", base_img);
219 }
220
221 #[test]
222 fn it_generates_captcha_using_builder() {
223 let start = std::time::Instant::now();
224 let captcha = CaptchaBuilder::new()
225 .length(5)
226 .width(200)
227 .height(70)
228 .dark_mode(false)
229 .complexity(5)
230 .compression(40)
231 .build();
232
233 let duration = start.elapsed();
234 println!("Time elapsed in generating captcha() is: {:?}", duration);
235
236 assert_eq!(captcha.text.len(), 5);
237 let base_img = captcha.to_base64();
238 assert!(base_img.starts_with("data:image/jpeg;base64,"));
239 println!("text: {}", captcha.text);
240 println!("base_img: {}", base_img);
241 }
242
243 #[test]
244 fn it_handles_empty_text_and_small_dimensions() {
245 let captcha = CaptchaBuilder::new()
246 .text(String::new())
247 .width(10)
248 .height(10)
249 .compression(0)
250 .build();
251
252 assert!(!captcha.text.is_empty());
253 let base_img = captcha.to_base64();
254 assert!(base_img.starts_with("data:image/jpeg;base64,"));
255 }
256
257 #[test]
258 fn it_generates_captcha_with_distortion() {
259 let captcha = CaptchaBuilder::new()
260 .text(String::from("wavy"))
261 .width(200)
262 .height(70)
263 .distortion(5)
264 .build();
265
266 assert_eq!(captcha.text, "wavy");
267 let base_img = captcha.to_base64();
268 assert!(base_img.starts_with("data:image/jpeg;base64,"));
269 }
270
271 #[test]
272 fn it_generates_captcha_with_custom_interference_and_shadow() {
273 let captcha = CaptchaBuilder::new()
274 .text(String::from("shadow"))
275 .width(200)
276 .height(70)
277 .drop_shadow(true)
278 .interference_lines(5)
279 .interference_ellipses(5)
280 .build();
281
282 assert_eq!(captcha.text, "shadow");
283 let base_img = captcha.to_base64();
284 assert!(base_img.starts_with("data:image/jpeg;base64,"));
285 }
286
287 #[test]
288 fn it_generates_captcha_with_custom_characters() {
289 let captcha = CaptchaBuilder::new()
290 .chars(vec!['A', 'B'])
291 .length(10)
292 .width(200)
293 .height(70)
294 .build();
295
296 assert_eq!(captcha.text.len(), 10);
297 assert!(captcha.text.chars().all(|c| c == 'A' || c == 'B'));
298 let base_img = captcha.to_base64();
299 assert!(base_img.starts_with("data:image/jpeg;base64,"));
300 }
301}