Skip to main content

captcha_rs/
lib.rs

1#![doc(html_root_url = "https://docs.rs/captcha-rs/latest")]
2
3//! Generate a verification image.
4//!
5//! ```rust
6//! use captcha_rs::{CaptchaBuilder};
7//!
8//! let captcha = CaptchaBuilder::new()
9//!     .length(5)
10//!     .width(130)
11//!     .height(40)
12//!     .dark_mode(false)
13//!     .complexity(1) // min: 1, max: 10
14//!     .compression(40) // min: 1, max: 99
15//!     .build();
16//!
17//! println!("text: {}", captcha.text);
18//! let base_img = captcha.to_base64();
19//! println!("base_img: {}", base_img);
20//! ```
21use 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        // Create a background image
146        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        // Loop to write the verification code string into the background image
151        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        // Draw interference lines
158        for _ in 0..self.interference_lines {
159            draw_interference_line(&mut image, self.dark_mode);
160        }
161
162        // Draw distraction circles
163        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}