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;
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        // Generate an array of captcha characters
74        let res = captcha::get_captcha(length);
75        self.text = Some(res.join(""));
76        self
77    }
78
79    pub fn width(mut self, width: u32) -> Self {
80        self.width = Some(width);
81        self
82    }
83
84    pub fn height(mut self, height: u32) -> Self {
85        self.height = Some(height);
86        self
87    }
88
89    pub fn dark_mode(mut self, dark_mode: bool) -> Self {
90        self.dark_mode = Some(dark_mode);
91        self
92    }
93
94    pub fn complexity(mut self, complexity: u32) -> Self {
95        let mut complexity = complexity;
96
97        if complexity > 10 {
98            complexity = 10;
99        }
100
101        if complexity < 1 {
102            complexity = 1;
103        }
104
105        self.complexity = Some(complexity);
106        self
107    }
108
109    pub fn compression(mut self, compression: u8) -> Self {
110        self.compression = Some(compression);
111        self
112    }
113
114    pub fn build(self) -> Captcha {
115        let text = self.text.unwrap_or(captcha::get_captcha(5).join(""));
116        let width = self.width.unwrap_or(130);
117        let height = self.height.unwrap_or(40);
118        let dark_mode = self.dark_mode.unwrap_or(false);
119        let complexity = self.complexity.unwrap_or(1);
120        let compression = self.compression.unwrap_or(40);
121
122        // Create a white background image
123        let mut image = get_image(width, height, dark_mode);
124
125        let res: Vec<String> = text.chars().map(|x| x.to_string()).collect();
126
127        // Loop to write the verification code string into the background image
128        cyclic_write_character(&res, &mut image, dark_mode);
129
130        // Draw interference lines
131        draw_interference_line(&mut image, dark_mode);
132        draw_interference_line(&mut image, dark_mode);
133
134        // Draw a distraction circle
135        draw_interference_ellipse(2, &mut image, dark_mode);
136        draw_interference_ellipse(2, &mut image, dark_mode);
137
138        if complexity > 1 {
139            let mut rng = rand::thread_rng();
140
141            gaussian_noise_mut(
142                &mut image,
143                (complexity - 1) as f64,
144                ((5 * complexity) - 5) as f64,
145                rng.gen(),
146            );
147
148            salt_and_pepper_noise_mut(&mut image, (0.002 * complexity as f64) - 0.002, rng.gen());
149        }
150
151        Captcha {
152            text,
153            image: DynamicImage::ImageRgb8(image),
154            compression,
155            dark_mode,
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use crate::CaptchaBuilder;
163
164    #[test]
165    fn it_generates_a_captcha() {
166        let _dark_mode = false;
167        let _text_length = 5;
168        let _width = 130;
169        let _height = 40;
170
171        let start = std::time::Instant::now();
172
173        let captcha = CaptchaBuilder::new()
174            .text(String::from("based"))
175            .width(200)
176            .height(70)
177            .dark_mode(false)
178            .build();
179
180        let duration = start.elapsed();
181        println!("Time elapsed in generating captcha() is: {:?}", duration);
182
183        assert_eq!(captcha.text.len(), 5);
184        let base_img = captcha.to_base64();
185        assert!(base_img.starts_with("data:image/jpeg;base64,"));
186        println!("text: {}", captcha.text);
187        println!("base_img: {}", base_img);
188    }
189
190    #[test]
191    fn it_generates_captcha_using_builder() {
192        let start = std::time::Instant::now();
193        let captcha = CaptchaBuilder::new()
194            .length(5)
195            .width(200)
196            .height(70)
197            .dark_mode(false)
198            .complexity(5)
199            .compression(40)
200            .build();
201
202        let duration = start.elapsed();
203        println!("Time elapsed in generating captcha() is: {:?}", duration);
204
205        assert_eq!(captcha.text.len(), 5);
206        let base_img = captcha.to_base64();
207        assert!(base_img.starts_with("data:image/jpeg;base64,"));
208        println!("text: {}", captcha.text);
209        println!("base_img: {}", base_img);
210    }
211}