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