ic_captcha/
lib.rs

1#![doc(html_root_url = "https://docs.rs/captcha-rs/latest")]
2
3//! Generate a verification image.
4//!
5//! ```rust
6//! use ic_captcha::CaptchaBuilder;
7//!
8//! let builder = CaptchaBuilder::new()
9//!   .length(4)
10//!   .width(140)
11//!   .height(60)
12//!   .mode(1)
13//!   .complexity(4);
14//!
15//! let captcha = builder.generate(b"random seed 0", None);
16//! println!("text: {}", captcha.text());
17//! println!("base_img: {}", captcha.to_base64(30));
18//! ```
19
20mod captcha;
21
22use captcha::Captcha;
23use sha3::{Digest, Sha3_256};
24
25/// The default font used to generate the captcha image.
26pub static FONTS: &[u8] = include_bytes!("../fonts/arial-rounded-bold.ttf");
27
28/// A builder struct for creating a [`Captcha`].
29pub struct CaptchaBuilder {
30    fonts: rusttype::Font<'static>,
31    length: u8,
32    width: u32,
33    height: u32,
34    mode: u8,
35    complexity: u32,
36}
37
38impl Default for CaptchaBuilder {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl CaptchaBuilder {
45    /// Returns a [`CaptchaBuilder`] with default configuration.
46    pub fn new() -> Self {
47        CaptchaBuilder {
48            length: 4,
49            fonts: rusttype::Font::try_from_bytes(FONTS).expect("Invalid font for CaptchaBuilder"),
50            width: 140,
51            height: 40,
52            mode: 1u8,
53            complexity: 5,
54        }
55    }
56
57    /// Set the length of the verification code string, default is 4.
58    pub fn length(mut self, length: u8) -> Self {
59        self.length = if length > 0 { length } else { 4 };
60        self
61    }
62
63    /// Set the font used to generate the captcha image, default is arial-rounded-bold.ttf.
64    pub fn fonts(mut self, fonts: rusttype::Font<'static>) -> Self {
65        self.fonts = fonts;
66        self
67    }
68
69    /// Set the width of the verification code image, default is 140.
70    pub fn width(mut self, width: u32) -> Self {
71        self.width = if width > 60 { width } else { 140 };
72        self
73    }
74
75    /// Set the height of the verification code image, default is 40.
76    pub fn height(mut self, height: u32) -> Self {
77        self.height = if height > 20 { height } else { 40 };
78        self
79    }
80
81    /// Set the color mode of the verification code image, default is 1.
82    /// 0: dark on light, 1: colorful on light, 2: colorful on dark.
83    pub fn mode(mut self, mode: u8) -> Self {
84        self.mode = mode;
85        self
86    }
87
88    /// Set the complexity of the verification code image, default is 5.
89    pub fn complexity(mut self, complexity: u32) -> Self {
90        self.complexity = if complexity > 10 {
91            10
92        } else if complexity < 1 {
93            1
94        } else {
95            complexity
96        };
97        self
98    }
99
100    /// Generate a [`Captcha`] with the given random seed and a optional text.
101    /// If the text is not provided, a text will be generated from random seed.
102    /// The random seed can be used only once. You should use a new seed for each new captcha.
103    pub fn generate(&self, seed: &[u8], text: Option<String>) -> Captcha {
104        let mut rnd = Rnd::new(seed);
105        let mut get_rnd_32 = |num: u32| rnd.rnd_32(num);
106        let mut captcha = match text {
107            Some(text) => Captcha::new(text, self.width, self.height, self.mode),
108            None => Captcha::random(
109                &mut get_rnd_32,
110                self.length,
111                self.width,
112                self.height,
113                self.mode,
114            ),
115        };
116
117        // Loop to write the verification code string into the background image
118        captcha.draw_characters(&mut get_rnd_32, &self.fonts);
119
120        let mut complexity = 1;
121        while complexity < self.complexity {
122            if complexity % 2 == 0 {
123                captcha.draw_interference_line(&mut get_rnd_32);
124            } else {
125                captcha.draw_interference_ellipse(&mut get_rnd_32);
126            }
127
128            complexity += 1;
129        }
130
131        captcha.draw_interference_noise(&mut get_rnd_32, self.complexity);
132
133        captcha
134    }
135}
136
137// A simple random number generator with a fixed seed
138struct Rnd {
139    offset: usize,
140    seed: [u8; 32],
141}
142
143impl Rnd {
144    fn new(seed: &[u8]) -> Self {
145        Rnd {
146            offset: 0,
147            seed: next_seed(seed),
148        }
149    }
150
151    // Generate a random number between 0 and num with the given seed
152    fn rnd_32(&mut self, num: u32) -> u32 {
153        let mut d = [0u8; 4];
154        d.copy_from_slice(&self.seed[self.offset..self.offset + 4]);
155        self.offset += 4;
156        if self.offset >= 32 {
157            self.seed = next_seed(&self.seed);
158            self.offset = 0;
159        }
160        u32::from_le_bytes(d) % num
161    }
162}
163
164// Generate a new seed from the given seed using SHA3-256
165fn next_seed(seed: &[u8]) -> [u8; 32] {
166    let mut hasher = Sha3_256::new();
167    hasher.update(seed);
168    hasher.finalize().into()
169}
170
171#[cfg(test)]
172mod tests {
173    use crate::CaptchaBuilder;
174
175    #[test]
176    fn it_generates_a_captcha() {
177        let builder = CaptchaBuilder::new();
178
179        let captcha = builder.generate(&[0u8, 32], None);
180        assert_eq!(captcha.text().as_str(), "UmfU");
181        let base_img = captcha.to_base64(0);
182        assert!(base_img.starts_with("data:image/jpeg;base64,"));
183        println!("text: {}", captcha.text());
184        println!("base_img: {}", base_img);
185
186        let captcha2 = builder.generate(&[0u8, 32], None);
187        assert_eq!(captcha2.text().as_str(), "UmfU");
188        assert_eq!(base_img, captcha2.to_base64(0));
189
190        let captcha2 = builder.generate(&[0u8, 32], Some("LDCLabs".to_string()));
191        assert_eq!(captcha2.text().as_str(), "LDCLabs");
192        assert_ne!(base_img, captcha2.to_base64(0));
193    }
194
195    #[test]
196    fn it_generates_captcha_using_builder() {
197        let captcha = CaptchaBuilder::new()
198            .length(4)
199            .width(120)
200            .height(60)
201            .mode(0)
202            .complexity(8)
203            .generate(&[1u8, 32], None);
204
205        assert_eq!(captcha.text().len(), 4);
206        let base_img = captcha.to_base64(10);
207        assert!(base_img.starts_with("data:image/jpeg;base64,"));
208        println!("text: {}", captcha.text());
209        println!("base_img: {}", base_img);
210    }
211}