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