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: 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        // Generate an array of captcha characters
82        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        // Create a background image
141        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        // Loop to write the verification code string into the background image
146        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        // Draw interference lines
153        for _ in 0..self.interference_lines {
154            draw_interference_line(&mut image, self.dark_mode);
155        }
156
157        // Draw distraction circles
158        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}