rucaptcha 3.2.5 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a40ae3f08b37c4b0ef9257665e88590f12f6ebf87a7bc7adaf600a51ab5689b
4
- data.tar.gz: e04d78453da659bb9d8de0b39d0c9d82e20145fa3b1b1ced5c4edde22425c223
3
+ metadata.gz: 6339aadd4896f5dd4e78a69bf4063105097b23d3172c0300ac0fa69f754ed68f
4
+ data.tar.gz: f68f939833e95450facc2481e5716cdd81acaa9a1fd7eebc12834d9e3a9e2b8c
5
5
  SHA512:
6
- metadata.gz: e53f34f7c6e3b30537f796d70e3675868c12ad7f668c9f1c625cc27f3eb629a4998b176870e7fb67891a072c798af7614e3a862fa2bb1f3fd27ad8c2e01dbc91
7
- data.tar.gz: 8832e227cbc5fc91491bfd4afa51b52d1f6d0b97b27e6b33b9c0b561895756d542528ea238a79f51731dde42620dc6d203085464fe7d5d45e1037a7dac906585
6
+ metadata.gz: 10dfd12f822bc09d72178947296ae78a44acdeda0069b6881429cc70cc3dda1b672c65e08576769cc3a5f9b40dce54d7898ad674a3e048688d15b7b5b9daac62
7
+ data.tar.gz: c3b8f573c381e61b4b9e7cec8262ad750eab938af67b52b6423eab3f1ab372c5c5b1a1c96730730bb92b23b431e732e7a185990030907633d9c528a7ef0bec43
data/README.md CHANGED
@@ -59,6 +59,16 @@ RuCaptcha.configure do
59
59
  # Enable or disable noise, default: false
60
60
  # self.noise = false
61
61
 
62
+ # Enable or disable circle background, default: true
63
+ # self.circle = true
64
+
65
+ # Set the difficulty level, default: 5, allows: [1..10].
66
+ # Only valid when noise is enabled
67
+ # self.difficulty = 5
68
+
69
+ # Set the case sensitive, default: false
70
+ # self.case_sensitive = false
71
+
62
72
  # Set the image format, default: png, allows: [jpeg, png, webp]
63
73
  # self.format = 'png'
64
74
 
@@ -181,11 +191,13 @@ end
181
191
 
182
192
  ## Performance
183
193
 
194
+ > Based on MacBook Pro (Apple M3)
195
+
184
196
  `rake benchmark` to run benchmark test.
185
197
 
186
198
  ```
187
199
  Warming up --------------------------------------
188
- Generate image 51.000 i/100ms
200
+ Generate image 84.000 i/100ms
189
201
  Calculating -------------------------------------
190
- Generate image 526.350 2.5%) i/s - 2.652k in 5.041681s
202
+ Generate image 859.075 1.5%) i/s (1.16 ms/i) - 4.368k in 5.085775s
191
203
  ```
data/Rakefile CHANGED
@@ -30,7 +30,7 @@ task default: :spec
30
30
 
31
31
  def create_captcha(length = 5, difficulty = 5)
32
32
  require "rucaptcha"
33
- RuCaptchaCore.create(length, difficulty, false, false, "png")
33
+ RuCaptchaCore.create(length, difficulty, false, false, false, "png")
34
34
  end
35
35
 
36
36
  task :preview do
@@ -63,10 +63,10 @@ task :benchmark do
63
63
  require "rucaptcha"
64
64
  require "benchmark/ips"
65
65
 
66
- RuCaptchaCore.create(5, 5, true, true, "png")
66
+ RuCaptchaCore.create(5, 5, true, true, true, "png")
67
67
 
68
68
  Benchmark.ips do |x|
69
- x.report("Generate image") { RuCaptchaCore.create(5, 5, true, true, "png") }
69
+ x.report("Generate image") { RuCaptchaCore.create(5, 5, true, true, true, "png") }
70
70
  x.compare!
71
71
  end
72
72
  end
@@ -1,7 +1,11 @@
1
1
  use image::{ImageBuffer, Rgba};
2
+ use imageproc::{
3
+ drawing::{draw_cubic_bezier_curve_mut, draw_filled_ellipse_mut},
4
+ noise::gaussian_noise_mut,
5
+ };
2
6
  use rand::{thread_rng, Rng};
3
7
  use rusttype::{Font, Scale};
4
- use std::io::Cursor;
8
+ use std::{io::Cursor, sync::LazyLock};
5
9
 
6
10
  static BASIC_CHAR: [char; 54] = [
7
11
  '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M',
@@ -9,9 +13,6 @@ static BASIC_CHAR: [char; 54] = [
9
13
  'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
10
14
  ];
11
15
 
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
16
  // https://coolors.co/cc0b8f-7c0abe-5700c8-3c2ea4-3d56a8-3fa67e-45bb30-69d003-a0d003-d8db02
16
17
  static COLORS: [(u8, u8, u8, u8); 14] = [
17
18
  (197, 166, 3, 255),
@@ -33,32 +34,34 @@ static COLORS: [(u8, u8, u8, u8); 14] = [
33
34
  static SCALE_SM: u32 = 32;
34
35
  static SCALE_MD: u32 = 45;
35
36
  static SCALE_LG: u32 = 55;
37
+ static FONT_0: LazyLock<Font> = LazyLock::new(|| {
38
+ Font::try_from_bytes(include_bytes!("../fonts/FuzzyBubbles-Regular.ttf")).unwrap()
39
+ });
40
+ static FONT_1: LazyLock<Font> =
41
+ LazyLock::new(|| Font::try_from_bytes(include_bytes!("../fonts/Handlee-Regular.ttf")).unwrap());
36
42
 
43
+ #[inline(always)]
37
44
  fn rand_num(len: usize) -> usize {
38
45
  let mut rng = thread_rng();
39
46
  rng.gen_range(0..=len)
40
47
  }
41
48
 
42
- fn get_captcha(len: usize) -> Vec<String> {
43
- let mut res = vec![];
49
+ /// Generate a random captcha string with a given length
50
+ #[inline]
51
+ fn rand_captcha(len: usize) -> String {
52
+ let mut result = String::with_capacity(len);
53
+ let seed = BASIC_CHAR.len() - 1;
44
54
  for _ in 0..len {
45
- let rnd = rand_num(53);
46
- res.push(BASIC_CHAR[rnd].to_string())
55
+ let rnd = rand_num(seed);
56
+ result.push(BASIC_CHAR[rnd])
47
57
  }
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])
58
+ result
56
59
  }
57
60
 
58
- fn get_colors(num: usize) -> Vec<Rgba<u8>> {
61
+ fn get_colors(len: usize) -> Vec<Rgba<u8>> {
59
62
  let rnd = rand_num(COLORS.len());
60
- let mut out = vec![];
61
- for i in 0..num {
63
+ let mut out = Vec::with_capacity(len);
64
+ for i in 0..len {
62
65
  let c = COLORS[(rnd + i) % COLORS.len()];
63
66
  out.push(Rgba([c.0, c.1, c.2, c.3]))
64
67
  }
@@ -66,78 +69,11 @@ fn get_colors(num: usize) -> Vec<Rgba<u8>> {
66
69
  out
67
70
  }
68
71
 
72
+ #[inline(always)]
69
73
  fn get_next(min: f32, max: u32) -> f32 {
70
74
  min + rand_num(max as usize - min as usize) as f32
71
75
  }
72
76
 
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
77
  fn draw_interference_line(num: usize, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, color: Rgba<u8>) {
142
78
  for _ in 0..num {
143
79
  let width = image.width();
@@ -154,7 +90,7 @@ fn draw_interference_line(num: usize, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>
154
90
  let ctrl_x2 = get_next((width / 12) as f32, width / 12 * 3);
155
91
  let ctrl_y2 = get_next(x1, height - 5);
156
92
  // Randomly draw bezier curves
157
- imageproc::drawing::draw_cubic_bezier_curve_mut(
93
+ draw_cubic_bezier_curve_mut(
158
94
  image,
159
95
  (x1, y1),
160
96
  (x2, y2),
@@ -176,7 +112,7 @@ fn draw_interference_ellipse(
176
112
  let x = rand_num((image.width() - 25) as usize) as i32;
177
113
  let y = rand_num((image.height() - 15) as usize) as i32;
178
114
 
179
- imageproc::drawing::draw_filled_ellipse_mut(image, (x, y), w, w, color);
115
+ draw_filled_ellipse_mut(image, (x, y), w, w, color);
180
116
  }
181
117
  }
182
118
 
@@ -187,16 +123,17 @@ pub struct Captcha {
187
123
 
188
124
  pub struct CaptchaBuilder {
189
125
  length: usize,
190
- width: usize,
191
- height: usize,
126
+ width: u32,
127
+ height: u32,
192
128
  complexity: usize,
193
129
  line: bool,
194
130
  noise: bool,
131
+ circle: bool,
195
132
  format: image::ImageFormat,
196
133
  }
197
134
 
198
- impl CaptchaBuilder {
199
- pub fn new() -> Self {
135
+ impl Default for CaptchaBuilder {
136
+ fn default() -> Self {
200
137
  CaptchaBuilder {
201
138
  length: 4,
202
139
  width: 220,
@@ -204,9 +141,16 @@ impl CaptchaBuilder {
204
141
  complexity: 5,
205
142
  line: true,
206
143
  noise: false,
144
+ circle: true,
207
145
  format: image::ImageFormat::Png,
208
146
  }
209
147
  }
148
+ }
149
+
150
+ impl CaptchaBuilder {
151
+ pub fn new() -> Self {
152
+ Self::default()
153
+ }
210
154
 
211
155
  pub fn length(mut self, length: usize) -> Self {
212
156
  self.length = length;
@@ -223,6 +167,11 @@ impl CaptchaBuilder {
223
167
  self
224
168
  }
225
169
 
170
+ pub fn circle(mut self, circle: bool) -> Self {
171
+ self.circle = circle;
172
+ self
173
+ }
174
+
226
175
  pub fn format(mut self, format: &str) -> Self {
227
176
  self.format = match format {
228
177
  "png" => image::ImageFormat::Png,
@@ -235,32 +184,89 @@ impl CaptchaBuilder {
235
184
  }
236
185
 
237
186
  pub fn complexity(mut self, complexity: usize) -> Self {
238
- let mut complexity = complexity;
239
- if complexity > 10 {
240
- complexity = 10;
187
+ self.complexity = complexity.clamp(1, 10);
188
+ self
189
+ }
190
+
191
+ fn cyclic_write_character(
192
+ &self,
193
+ captcha: &str,
194
+ image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
195
+ lines: bool,
196
+ ) {
197
+ let c = (image.width() - 20) / captcha.len() as u32;
198
+ let y = image.height() / 3 - 15;
199
+
200
+ let h = image.height() as f32;
201
+
202
+ let scale = match captcha.len() {
203
+ 1..=3 => SCALE_LG,
204
+ 4..=5 => SCALE_MD,
205
+ _ => SCALE_SM,
206
+ } as f32;
207
+
208
+ let colors = get_colors(captcha.len());
209
+ let line_colors = get_colors(captcha.len());
210
+
211
+ let xscale = scale - rand_num((scale * 0.2) as usize) as f32;
212
+ let yscale = h - rand_num((h * 0.2) as usize) as f32;
213
+
214
+ // Draw line, ellipse first as background
215
+ if self.circle {
216
+ (0..captcha.len()).for_each(|i| {
217
+ let line_color = line_colors[i];
218
+
219
+ if lines {
220
+ draw_interference_line(1, image, line_color);
221
+ }
222
+ draw_interference_ellipse(1, image, line_color);
223
+ });
241
224
  }
242
- if complexity < 1 {
243
- complexity = 1;
225
+
226
+ let font = match rand_num(2) {
227
+ 0 => &FONT_0,
228
+ 1 => &FONT_1,
229
+ _ => &FONT_1,
230
+ };
231
+
232
+ // Draw text
233
+ for (i, ch) in captcha.chars().enumerate() {
234
+ let color = colors[i];
235
+
236
+ for j in 0..(rand_num(3) + 1) as i32 {
237
+ // Draw text again with offset
238
+ let offset = j * (rand_num(2) as i32);
239
+ imageproc::drawing::draw_text_mut(
240
+ image,
241
+ color,
242
+ 10 + offset + (i as u32 * c) as i32,
243
+ y as i32,
244
+ Scale {
245
+ x: xscale + offset as f32,
246
+ y: yscale as f32,
247
+ },
248
+ font,
249
+ &ch.to_string(),
250
+ );
251
+ }
244
252
  }
245
- self.complexity = complexity;
246
- self
247
253
  }
248
254
 
249
255
  pub fn build(self) -> Captcha {
250
256
  // Generate an array of captcha characters
251
- let res = get_captcha(self.length);
252
-
253
- let text = res.join("");
257
+ let text = rand_captcha(self.length);
254
258
 
255
259
  // Create a white background image
256
- let mut image = get_image(self.width, self.height);
260
+ let mut buf = ImageBuffer::from_fn(self.width, self.height, |_, _| {
261
+ image::Rgba([255, 255, 255, 255])
262
+ });
257
263
 
258
264
  // Loop to write the verification code string into the background image
259
- cyclic_write_character(&res, &mut image, self.line);
265
+ self.cyclic_write_character(&text, &mut buf, self.line);
260
266
 
261
267
  if self.noise {
262
- imageproc::noise::gaussian_noise_mut(
263
- &mut image,
268
+ gaussian_noise_mut(
269
+ &mut buf,
264
270
  (self.complexity - 1) as f64,
265
271
  ((10 * self.complexity) - 10) as f64,
266
272
  ((5 * self.complexity) - 5) as u64,
@@ -268,9 +274,8 @@ impl CaptchaBuilder {
268
274
  }
269
275
 
270
276
  let mut bytes: Vec<u8> = Vec::new();
271
- image
272
- .write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)
273
- .unwrap();
277
+ buf.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)
278
+ .expect("failed to write rucaptcha image into png");
274
279
 
275
280
  Captcha { text, image: bytes }
276
281
  }
@@ -7,6 +7,7 @@ pub fn create(
7
7
  difficulty: usize,
8
8
  line: bool,
9
9
  noise: bool,
10
+ circle: bool,
10
11
  format: String,
11
12
  ) -> (String, Vec<u8>) {
12
13
  let c = captcha::CaptchaBuilder::new();
@@ -15,6 +16,7 @@ pub fn create(
15
16
  .length(len)
16
17
  .line(line)
17
18
  .noise(noise)
19
+ .circle(circle)
18
20
  .format(&format)
19
21
  .build();
20
22
 
@@ -24,7 +26,7 @@ pub fn create(
24
26
  #[magnus::init]
25
27
  fn init() -> Result<(), Error> {
26
28
  let class = define_class("RuCaptchaCore", magnus::class::object())?;
27
- class.define_singleton_method("create", function!(create, 5))?;
29
+ class.define_singleton_method("create", function!(create, 6))?;
28
30
 
29
31
  Ok(())
30
32
  }
@@ -13,11 +13,15 @@ module RuCaptcha
13
13
  attr_accessor :line
14
14
  # Enable or disable noise on captcha image, default: false
15
15
  attr_accessor :noise
16
+ # Enable or disable circle background on captcha image, default: true
17
+ attr_accessor :circle
16
18
  # Image format allow: ['jpeg', 'png', 'webp'], default: 'png'
17
19
  attr_accessor :format
18
20
  # skip_cache_store_check, default: false
19
21
  attr_accessor :skip_cache_store_check
20
22
  # custom rucaptcha mount path, default: '/rucaptcha'
21
23
  attr_accessor :mount_path
24
+ # Enable or disable case sensitive, default: false
25
+ attr_accessor :case_sensitive
22
26
  end
23
27
  end
@@ -62,10 +62,17 @@ module RuCaptcha
62
62
  return add_rucaptcha_validation_error if (Time.now.to_i - store_info[:time]) > RuCaptcha.config.expires_in
63
63
 
64
64
  # Make sure parama have captcha
65
- captcha = (opts[:captcha] || params[:_rucaptcha] || "").downcase.strip
65
+ captcha = (opts[:captcha] || params[:_rucaptcha] || "").strip
66
+ saved_code = store_info[:code]
67
+
66
68
  return add_rucaptcha_validation_error if captcha.blank?
67
69
 
68
- return add_rucaptcha_validation_error if captcha != store_info[:code]
70
+ unless RuCaptcha.config.case_sensitive
71
+ captcha.downcase!
72
+ saved_code.downcase!
73
+ end
74
+
75
+ return add_rucaptcha_validation_error if captcha != saved_code
69
76
 
70
77
  true
71
78
  end
@@ -1,3 +1,3 @@
1
1
  module RuCaptcha
2
- VERSION = "3.2.5"
2
+ VERSION = "3.3.0"
3
3
  end
data/lib/rucaptcha.rb CHANGED
@@ -29,9 +29,11 @@ module RuCaptcha
29
29
  @config.expires_in = 2.minutes
30
30
  @config.skip_cache_store_check = false
31
31
  @config.line = true
32
- @config.noise = true
32
+ @config.noise = false
33
+ @config.circle = true
33
34
  @config.format = "png"
34
35
  @config.mount_path = "/rucaptcha"
36
+ @config.case_sensitive = false
35
37
 
36
38
  @config.cache_store = if Rails.application
37
39
  Rails.application.config.cache_store
@@ -51,8 +53,12 @@ module RuCaptcha
51
53
 
52
54
  raise RuCaptcha::Errors::Configuration, "length config error, value must in 3..7" unless length.in?(3..7)
53
55
 
54
- result = RuCaptchaCore.create(length, config.difficulty || 5, config.line, config.noise, config.format)
55
- [result[0].downcase, result[1].pack("c*")]
56
+ result = RuCaptchaCore.create(length, config.difficulty || 5, config.line, config.noise, config.circle, config.format)
57
+ unless config.case_sensitive
58
+ result[0].downcase!
59
+ end
60
+
61
+ [result[0], result[1].pack("c*")]
56
62
  end
57
63
 
58
64
  def check_cache_store!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rucaptcha
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.5
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Lee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-30 00:00:00.000000000 Z
11
+ date: 2025-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties