1#![doc(html_root_url = "https://docs.rs/captcha-rs/latest")]
2
3mod captcha;
21
22use captcha::Captcha;
23use sha3::{Digest, Sha3_256};
24
25pub static FONTS: &[u8] = include_bytes!("../fonts/arial-rounded-bold.ttf");
27
28pub 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 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 pub fn length(mut self, length: u8) -> Self {
59 self.length = if length > 0 { length } else { 4 };
60 self
61 }
62
63 pub fn fonts(mut self, fonts: rusttype::Font<'static>) -> Self {
65 self.fonts = fonts;
66 self
67 }
68
69 pub fn width(mut self, width: u32) -> Self {
71 self.width = if width > 60 { width } else { 140 };
72 self
73 }
74
75 pub fn height(mut self, height: u32) -> Self {
77 self.height = if height > 20 { height } else { 40 };
78 self
79 }
80
81 pub fn mode(mut self, mode: u8) -> Self {
84 self.mode = mode;
85 self
86 }
87
88 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 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 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
137struct 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 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
164fn 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}