captcha_a/
captcha_builder.rs

1use image::{ImageBuffer, ImageResult, Rgba};
2use imageproc::drawing;
3use rand::{rngs::ThreadRng, Rng};
4use rusttype::{Font, Scale};
5use std::{fmt::Write, io::Cursor, path::Path};
6
7use crate::{color, Captcha};
8
9///the builder of captcha
10pub struct CaptchaBuilder<'a, 'b> {
11    ///captcha image width
12    pub width: u32,
13    ///captcha image height
14    pub height: u32,
15
16    ///random string length.
17    pub length: u32,
18
19    ///source is a unicode which is the rand string from.
20    pub source: String,
21
22    ///image background color (optional)
23    pub background_color: Option<Rgba<u8>>,
24    ///fonts collection for text
25    pub fonts: &'b [Font<'a>],
26    ///The maximum number of lines to draw behind of the image
27    pub max_behind_lines: Option<u32>,
28    ///The maximum number of lines to draw in front of the image
29    pub max_front_lines: Option<u32>,
30    ///The maximum number of ellipse lines to draw in front of the image
31    pub max_ellipse_lines: Option<u32>,
32}
33
34impl<'a, 'b> Default for CaptchaBuilder<'a, 'b> {
35    fn default() -> Self {
36        Self {
37            width: 150,
38            height: 40,
39            length: 5,
40            source: String::from("1234567890qwertyuioplkjhgfdsazxcvbnm"),
41            background_color: None,
42            fonts: &[],
43            max_behind_lines: None,
44            max_front_lines: None,
45            max_ellipse_lines: None,
46        }
47    }
48}
49
50impl<'a, 'b> CaptchaBuilder<'a, 'b> {
51    fn gen_random_text(&self, rng: &mut ThreadRng) -> String {
52        let mut source_chars = vec![];
53        for c in self.source.as_str().chars() {
54            source_chars.push(c);
55        }
56        let source_chars_count = source_chars.len();
57        let mut text = String::with_capacity(self.length as usize);
58        for _i in 0..self.length {
59            let r = rng.gen_range(0..source_chars_count);
60            write!(text, "{}", source_chars[r]).unwrap();
61        }
62        text
63    }
64    fn write_phrase(
65        &self,
66        image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
67        rng: &mut ThreadRng,
68        phrase: &str,
69    ) {
70        //println!("phrase={}", phrase);
71        //println!("width={}, height={}", self.width, self.height);
72        let font_size = (self.width as f32) / (self.length as f32) - rng.gen_range(1.0..=4.0);
73        let scale = Scale::uniform(font_size);
74        if self.fonts.is_empty() {
75            panic!("no fonts loaded");
76        }
77        let font_index = rng.gen_range(0..self.fonts.len());
78        let font = &self.fonts[font_index];
79        let glyphs: Vec<_> = font
80            .layout(phrase, scale, rusttype::point(0.0, 0.0))
81            .collect();
82        let text_height = {
83            let v_metrics = font.v_metrics(scale);
84            (v_metrics.ascent - v_metrics.descent).ceil() as u32
85        };
86        let text_width = {
87            let min_x = glyphs.first().unwrap().pixel_bounding_box().unwrap().min.x;
88            let max_x = glyphs.last().unwrap().pixel_bounding_box().unwrap().max.x;
89            let last_x_pos = glyphs.last().unwrap().position().x as i32;
90            (max_x + last_x_pos - min_x) as u32
91        };
92        let node_width = text_width / self.length;
93        //println!("text_width={}, text_height={}", text_width, text_height);
94        let mut x = ((self.width as i32) - (text_width as i32)) / 2;
95        let y = ((self.height as i32) - (text_height as i32)) / 2;
96        //
97        for s in phrase.chars() {
98            let text_color = color::gen_text_color(rng);
99            let offset = rng.gen_range(-5..=5);
100            //println!("x={}, y={}", x, y);
101            drawing::draw_text_mut(
102                image,
103                text_color,
104                x,
105                y + offset,
106                scale,
107                font,
108                &s.to_string(),
109            );
110            x += node_width as i32;
111        }
112    }
113
114    fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
115        let line_color = color::gen_line_color(rng);
116        let is_h = rng.gen();
117        let (start, end) = if is_h {
118            let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
119            let ya = rng.gen_range(0.0..(self.height as f32));
120            let xb = rng.gen_range((self.width as f32) / 2.0..(self.width as f32));
121            let yb = rng.gen_range(0.0..(self.height as f32));
122            ((xa, ya), (xb, yb))
123        } else {
124            let xa = rng.gen_range(0.0..(self.width as f32));
125            let ya = rng.gen_range(0.0..(self.height as f32) / 2.0);
126            let xb = rng.gen_range(0.0..(self.width as f32));
127            let yb = rng.gen_range((self.height as f32) / 2.0..(self.height as f32));
128            ((xa, ya), (xb, yb))
129        };
130        let thickness = rng.gen_range(2..4);
131        for i in 0..thickness {
132            let offset = i as f32;
133            if is_h {
134                drawing::draw_line_segment_mut(
135                    image,
136                    (start.0, start.1 + offset),
137                    (end.0, end.1 + offset),
138                    line_color,
139                );
140            } else {
141                drawing::draw_line_segment_mut(
142                    image,
143                    (start.0 + offset, start.1),
144                    (end.0 + offset, end.1),
145                    line_color,
146                );
147            }
148        }
149    }
150
151    fn draw_ellipse(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
152        let line_color = color::gen_line_color(rng);
153        let thickness = rng.gen_range(2..4);
154        for i in 0..thickness {
155            let center = (
156                rng.gen_range(-(self.width as i32) / 4..(self.width as i32) * 5 / 4),
157                rng.gen_range(-(self.height as i32) / 4..(self.height as i32) * 5 / 4),
158            );
159            drawing::draw_hollow_ellipse_mut(
160                image,
161                (center.0, center.1 + i),
162                (self.width * 6 / 7) as i32,
163                (self.height * 5 / 8) as i32,
164                line_color,
165            );
166        }
167    }
168
169    fn build_image(&self) -> (ImageBuffer<Rgba<u8>, Vec<u8>>, String) {
170        let mut rng = rand::thread_rng();
171        let bgc = match self.background_color {
172            Some(v) => v,
173            None => color::gen_background_color(&mut rng),
174        };
175        let mut image = ImageBuffer::from_fn(self.width, self.height, |_, _| bgc);
176        //draw behind line
177        let square = self.width * self.height;
178        let effects = match self.max_behind_lines {
179            Some(s) => {
180                if s > 0 {
181                    rng.gen_range(square / 3000..square / 2000).min(s)
182                } else {
183                    0
184                }
185            }
186            None => rng.gen_range(square / 3000..square / 2000),
187        };
188        for _ in 0..effects {
189            self.draw_line(&mut image, &mut rng);
190        }
191        //write phrase
192        let phrase = self.gen_random_text(&mut rng);
193        self.write_phrase(&mut image, &mut rng, &phrase);
194        //draw front line
195        let effects = match self.max_front_lines {
196            Some(s) => {
197                if s > 0 {
198                    rng.gen_range(square / 3000..=square / 2000).min(s)
199                } else {
200                    0
201                }
202            }
203            None => rng.gen_range(square / 3000..=square / 2000),
204        };
205        for _ in 0..effects {
206            self.draw_line(&mut image, &mut rng);
207        }
208        //draw ellipse
209        let effects = match self.max_front_lines {
210            Some(s) => {
211                if s > 0 {
212                    rng.gen_range(square / 4000..=square / 3000).min(s)
213                } else {
214                    0
215                }
216            }
217            None => rng.gen_range(square / 4000..=square / 3000),
218        };
219        for _ in 0..effects {
220            self.draw_ellipse(&mut image, &mut rng);
221        }
222        (image, phrase)
223    }
224
225    ///build a captcha in png format
226    pub fn build(&self) -> ImageResult<Captcha> {
227        let (image, phrase) = self.build_image();
228        let format = image::ImageOutputFormat::Png;
229        let mut raw_data: Vec<u8> = Vec::new();
230        image.write_to(&mut Cursor::new(&mut raw_data), format)?;
231        Ok(Captcha { raw_data, phrase })
232    }
233
234    ///build captcha and save in png format
235    pub fn save<P: AsRef<Path>>(&self, path: P) -> ImageResult<String> {
236        let (image, phrase) = self.build_image();
237        image.save(path)?;
238        Ok(phrase)
239    }
240}