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
9pub struct CaptchaBuilder<'a, 'b> {
11 pub width: u32,
13 pub height: u32,
15
16 pub length: u32,
18
19 pub source: String,
21
22 pub background_color: Option<Rgba<u8>>,
24 pub fonts: &'b [Font<'a>],
26 pub max_behind_lines: Option<u32>,
28 pub max_front_lines: Option<u32>,
30 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 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 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 for s in phrase.chars() {
98 let text_color = color::gen_text_color(rng);
99 let offset = rng.gen_range(-5..=5);
100 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 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 let phrase = self.gen_random_text(&mut rng);
193 self.write_phrase(&mut image, &mut rng, &phrase);
194 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 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 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 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}