daqing_rucaptcha 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +188 -0
- data/Rakefile +71 -0
- data/app/controllers/ru_captcha/captcha_controller.rb +14 -0
- data/config/locales/rucaptcha.de-DE.yml +3 -0
- data/config/locales/rucaptcha.en.yml +3 -0
- data/config/locales/rucaptcha.pt-BR.yml +3 -0
- data/config/locales/rucaptcha.zh-CN.yml +3 -0
- data/config/locales/rucaptcha.zh-TW.yml +3 -0
- data/config/routes.rb +3 -0
- data/ext/rucaptcha/Cargo.lock +1226 -0
- data/ext/rucaptcha/Cargo.toml +14 -0
- data/ext/rucaptcha/extconf.rb +4 -0
- data/ext/rucaptcha/fonts/FuzzyBubbles-Regular.ttf +0 -0
- data/ext/rucaptcha/fonts/Handlee-Regular.ttf +0 -0
- data/ext/rucaptcha/src/captcha.rs +341 -0
- data/ext/rucaptcha/src/lib.rs +30 -0
- data/ext/rucaptcha/target/release/build/clang-sys-f87b799a4d35af4b/out/common.rs +355 -0
- data/ext/rucaptcha/target/release/build/clang-sys-f87b799a4d35af4b/out/dynamic.rs +257 -0
- data/ext/rucaptcha/target/release/build/clang-sys-f87b799a4d35af4b/out/macros.rs +38 -0
- data/ext/rucaptcha/target/release/build/num-bigint-34c75daf72c2b049/out/radix_bases.rs +780 -0
- data/ext/rucaptcha/target/release/build/rb-sys-6bdd5b2895b9570a/out/bindings-0.9.78-arm64-darwin22-3.2.2.rs +19775 -0
- data/ext/rucaptcha/target/release/build/typenum-f3872660002fb991/out/consts.rs +2248 -0
- data/ext/rucaptcha/target/release/build/typenum-f3872660002fb991/out/op.rs +1030 -0
- data/ext/rucaptcha/target/release/build/typenum-f3872660002fb991/out/tests.rs +20565 -0
- data/lib/rucaptcha/cache.rb +12 -0
- data/lib/rucaptcha/configuration.rb +21 -0
- data/lib/rucaptcha/controller_helpers.rb +90 -0
- data/lib/rucaptcha/engine.rb +15 -0
- data/lib/rucaptcha/errors/configuration.rb +5 -0
- data/lib/rucaptcha/version.rb +3 -0
- data/lib/rucaptcha/view_helpers.rb +22 -0
- data/lib/rucaptcha.rb +86 -0
- metadata +103 -0
Binary file
|
Binary file
|
@@ -0,0 +1,341 @@
|
|
1
|
+
use image::{ImageBuffer, Rgba};
|
2
|
+
use rand::{thread_rng, Rng};
|
3
|
+
use rusttype::{Font, Scale};
|
4
|
+
use std::io::Cursor;
|
5
|
+
|
6
|
+
static BASIC_CHAR: [char; 54] = [
|
7
|
+
'2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M',
|
8
|
+
'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
9
|
+
'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
10
|
+
];
|
11
|
+
|
12
|
+
static FONT_BYTES1: &[u8; 145008] = include_bytes!("../fonts/FuzzyBubbles-Regular.ttf");
|
13
|
+
static FONT_BYTES2: &[u8; 37792] = include_bytes!("../fonts/Handlee-Regular.ttf");
|
14
|
+
|
15
|
+
// https://coolors.co/cc0b8f-7c0abe-5700c8-3c2ea4-3d56a8-3fa67e-45bb30-69d003-a0d003-d8db02
|
16
|
+
static COLORS: [(u8, u8, u8, u8); 14] = [
|
17
|
+
(197, 166, 3, 255),
|
18
|
+
(187, 87, 5, 255),
|
19
|
+
(176, 7, 7, 255),
|
20
|
+
(186, 9, 56, 255),
|
21
|
+
(204, 11, 143, 255),
|
22
|
+
(124, 10, 190, 255),
|
23
|
+
(87, 0, 200, 255),
|
24
|
+
(61, 86, 168, 255),
|
25
|
+
(63, 166, 126, 255),
|
26
|
+
(69, 187, 48, 255),
|
27
|
+
(105, 208, 3, 255),
|
28
|
+
(160, 208, 3, 255),
|
29
|
+
(216, 219, 2, 255),
|
30
|
+
(50, 50, 50, 255),
|
31
|
+
];
|
32
|
+
|
33
|
+
static SCALE_SM: u32 = 32;
|
34
|
+
static SCALE_MD: u32 = 45;
|
35
|
+
static SCALE_LG: u32 = 55;
|
36
|
+
|
37
|
+
fn rand_num(len: usize) -> usize {
|
38
|
+
let mut rng = thread_rng();
|
39
|
+
rng.gen_range(0..=len)
|
40
|
+
}
|
41
|
+
|
42
|
+
fn get_captcha(len: usize) -> Vec<String> {
|
43
|
+
let mut res = vec![];
|
44
|
+
for _ in 0..len {
|
45
|
+
let rnd = rand_num(53);
|
46
|
+
res.push(BASIC_CHAR[rnd].to_string())
|
47
|
+
}
|
48
|
+
res
|
49
|
+
}
|
50
|
+
|
51
|
+
#[allow(unused)]
|
52
|
+
fn get_color() -> Rgba<u8> {
|
53
|
+
let rnd = rand_num(COLORS.len() - 1);
|
54
|
+
let c = COLORS[rnd];
|
55
|
+
Rgba([c.0, c.1, c.2, c.3])
|
56
|
+
}
|
57
|
+
|
58
|
+
fn get_colors(num: usize) -> Vec<Rgba<u8>> {
|
59
|
+
let rnd = rand_num(COLORS.len());
|
60
|
+
let mut out = vec![];
|
61
|
+
for i in 0..num {
|
62
|
+
let c = COLORS[(rnd + i) % COLORS.len()];
|
63
|
+
out.push(Rgba([c.0, c.1, c.2, c.3]))
|
64
|
+
}
|
65
|
+
|
66
|
+
out
|
67
|
+
}
|
68
|
+
|
69
|
+
fn get_next(min: f32, max: u32) -> f32 {
|
70
|
+
min + rand_num(max as usize - min as usize) as f32
|
71
|
+
}
|
72
|
+
|
73
|
+
fn get_font() -> Font<'static> {
|
74
|
+
match rand_num(2) {
|
75
|
+
0 => Font::try_from_bytes(FONT_BYTES1).unwrap(),
|
76
|
+
1 => Font::try_from_bytes(FONT_BYTES2).unwrap(),
|
77
|
+
_ => Font::try_from_bytes(FONT_BYTES1).unwrap(),
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
fn get_image(width: usize, height: usize) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
|
82
|
+
ImageBuffer::from_fn(width as u32, height as u32, |_, _| {
|
83
|
+
image::Rgba([255, 255, 255, 255])
|
84
|
+
})
|
85
|
+
}
|
86
|
+
|
87
|
+
fn cyclic_write_character(res: &[String], image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, lines: bool) {
|
88
|
+
let c = (image.width() - 20) / res.len() as u32;
|
89
|
+
let y = image.height() / 3 - 15;
|
90
|
+
|
91
|
+
let h = image.height() as f32;
|
92
|
+
|
93
|
+
let scale = match res.len() {
|
94
|
+
1..=3 => SCALE_LG,
|
95
|
+
4..=5 => SCALE_MD,
|
96
|
+
_ => SCALE_SM,
|
97
|
+
} as f32;
|
98
|
+
|
99
|
+
let colors = get_colors(res.len());
|
100
|
+
let line_colors = get_colors(res.len());
|
101
|
+
|
102
|
+
let xscale = scale - rand_num((scale * 0.2) as usize) as f32;
|
103
|
+
let yscale = h as f32 - rand_num((h * 0.2) as usize) as f32;
|
104
|
+
|
105
|
+
// Draw line, ellipse first as background
|
106
|
+
for (i, _) in res.iter().enumerate() {
|
107
|
+
let line_color = line_colors[i];
|
108
|
+
|
109
|
+
if lines {
|
110
|
+
draw_interference_line(1, image, line_color);
|
111
|
+
}
|
112
|
+
draw_interference_ellipse(1, image, line_color);
|
113
|
+
}
|
114
|
+
|
115
|
+
// Draw text
|
116
|
+
for (i, _) in res.iter().enumerate() {
|
117
|
+
let text = &res[i];
|
118
|
+
|
119
|
+
let color = colors[i];
|
120
|
+
let font = get_font();
|
121
|
+
|
122
|
+
for j in 0..(rand_num(3) + 1) as i32 {
|
123
|
+
// Draw text again with offset
|
124
|
+
let offset = j * (rand_num(2) as i32);
|
125
|
+
imageproc::drawing::draw_text_mut(
|
126
|
+
image,
|
127
|
+
color,
|
128
|
+
10 + offset + (i as u32 * c) as i32,
|
129
|
+
y as i32 as i32,
|
130
|
+
Scale {
|
131
|
+
x: xscale + offset as f32,
|
132
|
+
y: yscale as f32,
|
133
|
+
},
|
134
|
+
&font,
|
135
|
+
text,
|
136
|
+
);
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
fn draw_interference_line(num: usize, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, color: Rgba<u8>) {
|
142
|
+
for _ in 0..num {
|
143
|
+
let width = image.width();
|
144
|
+
let height = image.height();
|
145
|
+
let x1: f32 = 5.0;
|
146
|
+
let y1 = get_next(x1, height / 2);
|
147
|
+
|
148
|
+
let x2 = (width - 5) as f32;
|
149
|
+
let y2 = get_next(5.0, height - 5);
|
150
|
+
|
151
|
+
let ctrl_x = get_next((width / 6) as f32, width / 4 * 3);
|
152
|
+
let ctrl_y = get_next(x1, height - 5);
|
153
|
+
|
154
|
+
let ctrl_x2 = get_next((width / 12) as f32, width / 12 * 3);
|
155
|
+
let ctrl_y2 = get_next(x1, height - 5);
|
156
|
+
// Randomly draw bezier curves
|
157
|
+
imageproc::drawing::draw_cubic_bezier_curve_mut(
|
158
|
+
image,
|
159
|
+
(x1, y1),
|
160
|
+
(x2, y2),
|
161
|
+
(ctrl_x, ctrl_y),
|
162
|
+
(ctrl_x2, ctrl_y2),
|
163
|
+
color,
|
164
|
+
);
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
fn draw_interference_ellipse(
|
169
|
+
num: usize,
|
170
|
+
image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
|
171
|
+
color: Rgba<u8>,
|
172
|
+
) {
|
173
|
+
for _ in 0..num {
|
174
|
+
// max cycle width 20px
|
175
|
+
let w = (10 + rand_num(10)) as i32;
|
176
|
+
let x = rand_num((image.width() - 25) as usize) as i32;
|
177
|
+
let y = rand_num((image.height() - 15) as usize) as i32;
|
178
|
+
|
179
|
+
imageproc::drawing::draw_filled_ellipse_mut(image, (x, y), w, w, color);
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
pub struct Captcha {
|
184
|
+
pub text: String,
|
185
|
+
pub image: Vec<u8>,
|
186
|
+
}
|
187
|
+
|
188
|
+
pub struct CaptchaBuilder {
|
189
|
+
length: usize,
|
190
|
+
width: usize,
|
191
|
+
height: usize,
|
192
|
+
complexity: usize,
|
193
|
+
line: bool,
|
194
|
+
noise: bool,
|
195
|
+
format: image::ImageFormat,
|
196
|
+
}
|
197
|
+
|
198
|
+
impl CaptchaBuilder {
|
199
|
+
pub fn new() -> Self {
|
200
|
+
CaptchaBuilder {
|
201
|
+
length: 4,
|
202
|
+
width: 220,
|
203
|
+
height: 70,
|
204
|
+
complexity: 5,
|
205
|
+
line: true,
|
206
|
+
noise: false,
|
207
|
+
format: image::ImageFormat::Png,
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
pub fn length(mut self, length: usize) -> Self {
|
212
|
+
self.length = length;
|
213
|
+
self
|
214
|
+
}
|
215
|
+
|
216
|
+
pub fn line(mut self, line: bool) -> Self {
|
217
|
+
self.line = line;
|
218
|
+
self
|
219
|
+
}
|
220
|
+
|
221
|
+
pub fn noise(mut self, noise: bool) -> Self {
|
222
|
+
self.noise = noise;
|
223
|
+
self
|
224
|
+
}
|
225
|
+
|
226
|
+
pub fn format(mut self, format: &str) -> Self {
|
227
|
+
self.format = match format {
|
228
|
+
"png" => image::ImageFormat::Png,
|
229
|
+
"jpg" | "jpeg" => image::ImageFormat::Jpeg,
|
230
|
+
"webp" => image::ImageFormat::WebP,
|
231
|
+
_ => image::ImageFormat::Png,
|
232
|
+
};
|
233
|
+
|
234
|
+
self
|
235
|
+
}
|
236
|
+
|
237
|
+
pub fn complexity(mut self, complexity: usize) -> Self {
|
238
|
+
let mut complexity = complexity;
|
239
|
+
if complexity > 10 {
|
240
|
+
complexity = 10;
|
241
|
+
}
|
242
|
+
if complexity < 1 {
|
243
|
+
complexity = 1;
|
244
|
+
}
|
245
|
+
self.complexity = complexity;
|
246
|
+
self
|
247
|
+
}
|
248
|
+
|
249
|
+
pub fn build(self) -> Captcha {
|
250
|
+
// Generate an array of captcha characters
|
251
|
+
let res = get_captcha(self.length);
|
252
|
+
|
253
|
+
let text = res.join("");
|
254
|
+
|
255
|
+
// Create a white background image
|
256
|
+
let mut image = get_image(self.width, self.height);
|
257
|
+
|
258
|
+
// Loop to write the verification code string into the background image
|
259
|
+
cyclic_write_character(&res, &mut image, self.line);
|
260
|
+
|
261
|
+
if self.noise {
|
262
|
+
imageproc::noise::gaussian_noise_mut(
|
263
|
+
&mut image,
|
264
|
+
(self.complexity - 1) as f64,
|
265
|
+
((10 * self.complexity) - 10) as f64,
|
266
|
+
((5 * self.complexity) - 5) as u64,
|
267
|
+
);
|
268
|
+
}
|
269
|
+
|
270
|
+
let mut bytes: Vec<u8> = Vec::new();
|
271
|
+
image
|
272
|
+
.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)
|
273
|
+
.unwrap();
|
274
|
+
|
275
|
+
Captcha { text, image: bytes }
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
#[cfg(test)]
|
280
|
+
mod tests {
|
281
|
+
use super::*;
|
282
|
+
|
283
|
+
#[test]
|
284
|
+
fn test_format() {
|
285
|
+
let mut builder = CaptchaBuilder::new();
|
286
|
+
assert_eq!(builder.format, image::ImageFormat::Png);
|
287
|
+
|
288
|
+
builder = builder.format("jpg");
|
289
|
+
assert_eq!(builder.format, image::ImageFormat::Jpeg);
|
290
|
+
builder = builder.format("jpeg");
|
291
|
+
assert_eq!(builder.format, image::ImageFormat::Jpeg);
|
292
|
+
builder = builder.format("webp");
|
293
|
+
assert_eq!(builder.format, image::ImageFormat::WebP);
|
294
|
+
builder = builder.format("png");
|
295
|
+
assert_eq!(builder.format, image::ImageFormat::Png);
|
296
|
+
builder = builder.format("gif");
|
297
|
+
assert_eq!(builder.format, image::ImageFormat::Png);
|
298
|
+
}
|
299
|
+
|
300
|
+
#[test]
|
301
|
+
fn test_line() {
|
302
|
+
let mut builder = CaptchaBuilder::new();
|
303
|
+
assert!(builder.line);
|
304
|
+
|
305
|
+
builder = builder.line(false);
|
306
|
+
assert!(!builder.line);
|
307
|
+
}
|
308
|
+
|
309
|
+
#[test]
|
310
|
+
fn test_noise() {
|
311
|
+
let mut builder = CaptchaBuilder::new();
|
312
|
+
assert!(!builder.noise);
|
313
|
+
|
314
|
+
builder = builder.noise(true);
|
315
|
+
assert!(builder.noise);
|
316
|
+
}
|
317
|
+
|
318
|
+
#[test]
|
319
|
+
fn test_difficulty() {
|
320
|
+
let mut builder = CaptchaBuilder::new();
|
321
|
+
assert_eq!(builder.complexity, 5);
|
322
|
+
|
323
|
+
builder = builder.complexity(10);
|
324
|
+
assert_eq!(builder.complexity, 10);
|
325
|
+
|
326
|
+
builder = builder.complexity(11);
|
327
|
+
assert_eq!(builder.complexity, 10);
|
328
|
+
|
329
|
+
builder = builder.complexity(0);
|
330
|
+
assert_eq!(builder.complexity, 1);
|
331
|
+
}
|
332
|
+
|
333
|
+
#[test]
|
334
|
+
fn test_length() {
|
335
|
+
let mut builder = CaptchaBuilder::new();
|
336
|
+
assert_eq!(builder.length, 4);
|
337
|
+
|
338
|
+
builder = builder.length(10);
|
339
|
+
assert_eq!(builder.length, 10);
|
340
|
+
}
|
341
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
use magnus::{define_class, function, Error, Object};
|
2
|
+
|
3
|
+
mod captcha;
|
4
|
+
|
5
|
+
pub fn create(
|
6
|
+
len: usize,
|
7
|
+
difficulty: usize,
|
8
|
+
line: bool,
|
9
|
+
noise: bool,
|
10
|
+
format: String,
|
11
|
+
) -> (String, Vec<u8>) {
|
12
|
+
let c = captcha::CaptchaBuilder::new();
|
13
|
+
let out = c
|
14
|
+
.complexity(difficulty)
|
15
|
+
.length(len)
|
16
|
+
.line(line)
|
17
|
+
.noise(noise)
|
18
|
+
.format(&format)
|
19
|
+
.build();
|
20
|
+
|
21
|
+
(out.text, out.image)
|
22
|
+
}
|
23
|
+
|
24
|
+
#[magnus::init]
|
25
|
+
fn init() -> Result<(), Error> {
|
26
|
+
let class = define_class("RuCaptchaCore", Default::default())?;
|
27
|
+
class.define_singleton_method("create", function!(create, 5))?;
|
28
|
+
|
29
|
+
Ok(())
|
30
|
+
}
|