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    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 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        // Create a white background image
129        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        // Loop to write the verification code string into the background image
134        cyclic_write_character(&res, &mut image, dark_mode);
135
136        // Draw interference lines
137        draw_interference_line(&mut image, dark_mode);
138        draw_interference_line(&mut image, dark_mode);
139
140        // Draw a distraction circle
141        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}