flask-Humanify 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (28) hide show
  1. {flask_humanify-0.2.0/flask_Humanify.egg-info → flask_humanify-0.2.1}/PKG-INFO +35 -2
  2. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/README.md +34 -1
  3. {flask_humanify-0.2.0 → flask_humanify-0.2.1/flask_Humanify.egg-info}/PKG-INFO +35 -2
  4. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/SOURCES.txt +1 -1
  5. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/__init__.py +1 -1
  6. flask_humanify-0.2.1/flask_humanify/datasets/keys.pkl +0 -0
  7. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/humanify.py +183 -21
  8. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/memory_server.py +5 -3
  9. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/utils.py +86 -20
  10. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/pyproject.toml +1 -1
  11. flask_humanify-0.2.0/flask_humanify/secret_key.bin +0 -0
  12. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/LICENSE +0 -0
  13. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/MANIFEST.in +0 -0
  14. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/dependency_links.txt +0 -0
  15. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/requires.txt +0 -0
  16. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/top_level.txt +0 -0
  17. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/ai_dogs.pkl +0 -0
  18. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/animals.pkl +0 -0
  19. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/characters.pkl +0 -0
  20. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/ipset.json +0 -0
  21. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/features/rate_limiter.py +0 -0
  22. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/access_denied.html +0 -0
  23. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/audio_challenge.html +0 -0
  24. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/grid_challenge.html +0 -0
  25. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/one_click_challenge.html +0 -0
  26. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/rate_limited.html +0 -0
  27. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/setup.cfg +0 -0
  28. {flask_humanify-0.2.0 → flask_humanify-0.2.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-Humanify
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Protect against bots and DDoS attacks
5
5
  Author-email: TN3W <tn3w@protonmail.com>
6
6
  License-Expression: Apache-2.0
@@ -45,7 +45,7 @@ from flask import Flask
45
45
  from flask_Humanify import Humanify
46
46
 
47
47
  app = Flask(__name__)
48
- humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
48
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
49
49
 
50
50
  # Register the middleware to deny access to bots
51
51
  humanify.register_middleware(action="challenge")
@@ -61,6 +61,39 @@ if __name__ == "__main__":
61
61
  app.run()
62
62
  ```
63
63
 
64
+ ### Advanced Protection Rules
65
+
66
+ You can customize bot protection with advanced filtering rules:
67
+
68
+ ```python
69
+ # Protect specific endpoints with regex patterns
70
+ humanify.register_middleware(
71
+ action="challenge",
72
+ endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
73
+ )
74
+
75
+ # Protect specific URL paths
76
+ humanify.register_middleware(
77
+ action="deny_access",
78
+ url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
79
+ )
80
+
81
+ # Exclude certain patterns from protection
82
+ humanify.register_middleware(
83
+ endpoint_patterns=["api.*"],
84
+ exclude_patterns=["api.public.*"] # Don't protect public API endpoints
85
+ )
86
+
87
+ # Filter by request parameters
88
+ humanify.register_middleware(
89
+ request_filters={
90
+ "method": ["POST", "PUT", "DELETE"], # Only protect write operations
91
+ "args.admin": "true", # Only when admin=true query parameter exists
92
+ "headers.content-type": "regex:application/json.*" # Match content type with regex
93
+ }
94
+ )
95
+ ```
96
+
64
97
  Not using the middleware:
65
98
 
66
99
  ```python
@@ -9,7 +9,7 @@ from flask import Flask
9
9
  from flask_Humanify import Humanify
10
10
 
11
11
  app = Flask(__name__)
12
- humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
12
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
13
13
 
14
14
  # Register the middleware to deny access to bots
15
15
  humanify.register_middleware(action="challenge")
@@ -25,6 +25,39 @@ if __name__ == "__main__":
25
25
  app.run()
26
26
  ```
27
27
 
28
+ ### Advanced Protection Rules
29
+
30
+ You can customize bot protection with advanced filtering rules:
31
+
32
+ ```python
33
+ # Protect specific endpoints with regex patterns
34
+ humanify.register_middleware(
35
+ action="challenge",
36
+ endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
37
+ )
38
+
39
+ # Protect specific URL paths
40
+ humanify.register_middleware(
41
+ action="deny_access",
42
+ url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
43
+ )
44
+
45
+ # Exclude certain patterns from protection
46
+ humanify.register_middleware(
47
+ endpoint_patterns=["api.*"],
48
+ exclude_patterns=["api.public.*"] # Don't protect public API endpoints
49
+ )
50
+
51
+ # Filter by request parameters
52
+ humanify.register_middleware(
53
+ request_filters={
54
+ "method": ["POST", "PUT", "DELETE"], # Only protect write operations
55
+ "args.admin": "true", # Only when admin=true query parameter exists
56
+ "headers.content-type": "regex:application/json.*" # Match content type with regex
57
+ }
58
+ )
59
+ ```
60
+
28
61
  Not using the middleware:
29
62
 
30
63
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-Humanify
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Protect against bots and DDoS attacks
5
5
  Author-email: TN3W <tn3w@protonmail.com>
6
6
  License-Expression: Apache-2.0
@@ -45,7 +45,7 @@ from flask import Flask
45
45
  from flask_Humanify import Humanify
46
46
 
47
47
  app = Flask(__name__)
48
- humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
48
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
49
49
 
50
50
  # Register the middleware to deny access to bots
51
51
  humanify.register_middleware(action="challenge")
@@ -61,6 +61,39 @@ if __name__ == "__main__":
61
61
  app.run()
62
62
  ```
63
63
 
64
+ ### Advanced Protection Rules
65
+
66
+ You can customize bot protection with advanced filtering rules:
67
+
68
+ ```python
69
+ # Protect specific endpoints with regex patterns
70
+ humanify.register_middleware(
71
+ action="challenge",
72
+ endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
73
+ )
74
+
75
+ # Protect specific URL paths
76
+ humanify.register_middleware(
77
+ action="deny_access",
78
+ url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
79
+ )
80
+
81
+ # Exclude certain patterns from protection
82
+ humanify.register_middleware(
83
+ endpoint_patterns=["api.*"],
84
+ exclude_patterns=["api.public.*"] # Don't protect public API endpoints
85
+ )
86
+
87
+ # Filter by request parameters
88
+ humanify.register_middleware(
89
+ request_filters={
90
+ "method": ["POST", "PUT", "DELETE"], # Only protect write operations
91
+ "args.admin": "true", # Only when admin=true query parameter exists
92
+ "headers.content-type": "regex:application/json.*" # Match content type with regex
93
+ }
94
+ )
95
+ ```
96
+
64
97
  Not using the middleware:
65
98
 
66
99
  ```python
@@ -11,12 +11,12 @@ flask_Humanify.egg-info/top_level.txt
11
11
  flask_humanify/__init__.py
12
12
  flask_humanify/humanify.py
13
13
  flask_humanify/memory_server.py
14
- flask_humanify/secret_key.bin
15
14
  flask_humanify/utils.py
16
15
  flask_humanify/datasets/ai_dogs.pkl
17
16
  flask_humanify/datasets/animals.pkl
18
17
  flask_humanify/datasets/characters.pkl
19
18
  flask_humanify/datasets/ipset.json
19
+ flask_humanify/datasets/keys.pkl
20
20
  flask_humanify/features/rate_limiter.py
21
21
  flask_humanify/templates/access_denied.html
22
22
  flask_humanify/templates/audio_challenge.html
@@ -4,7 +4,7 @@ Flask-Humanify
4
4
  A Flask extension that protects against bots and DDoS attacks.
5
5
  """
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.2.1"
8
8
 
9
9
  from . import utils
10
10
  from .humanify import Humanify
@@ -1,7 +1,9 @@
1
1
  from dataclasses import dataclass
2
2
  import logging
3
3
  import random
4
- from typing import List, Optional
4
+ from typing import List, Optional, Union, Dict, Any, Pattern
5
+ import re
6
+ import fnmatch
5
7
 
6
8
  from werkzeug.wrappers import Response
7
9
  from flask import (
@@ -46,13 +48,13 @@ IMAGE_CAPTCHA_MAPPING = {
46
48
  "num_correct": (2, 3),
47
49
  "num_images": 9,
48
50
  "preview_image": False,
49
- "hardness_range": (1, 4),
51
+ "hardness_range": (1, 3),
50
52
  },
51
53
  "one_click": {
52
54
  "num_correct": 1,
53
55
  "num_images": 6,
54
56
  "preview_image": True,
55
- "hardness_range": (1, 2),
57
+ "hardness_range": (1, 3),
56
58
  },
57
59
  }
58
60
 
@@ -127,11 +129,16 @@ class Humanify:
127
129
  """
128
130
 
129
131
  def __init__(
130
- self, app=None, challenge_type: str = "one_click", captcha_dataset: str = "ai_dogs"
132
+ self,
133
+ app=None,
134
+ challenge_type: str = "one_click",
135
+ image_dataset: Optional[str] = "ai_dogs",
136
+ audio_dataset: Optional[str] = None,
131
137
  ):
132
138
  self.app = app
133
139
  self.challenge_type = challenge_type
134
- self.captcha_dataset = captcha_dataset
140
+ self.image_dataset = image_dataset
141
+ self.audio_dataset = audio_dataset
135
142
  if app is not None:
136
143
  self.init_app(app)
137
144
 
@@ -142,7 +149,8 @@ class Humanify:
142
149
  self.app = app
143
150
 
144
151
  ensure_server_running(
145
- image_dataset=self.captcha_dataset,
152
+ image_dataset=self.image_dataset,
153
+ audio_dataset=self.audio_dataset,
146
154
  )
147
155
  self.memory_client = MemoryClient()
148
156
  self.memory_client.connect()
@@ -162,6 +170,9 @@ class Humanify:
162
170
  """
163
171
  Challenge route.
164
172
  """
173
+ if self.image_dataset is None:
174
+ return self._render_challenge(is_audio=True)
175
+
165
176
  return self._render_challenge()
166
177
 
167
178
  @self.blueprint.route("/humanify/audio_challenge", methods=["GET"])
@@ -169,6 +180,11 @@ class Humanify:
169
180
  """
170
181
  Audio challenge route.
171
182
  """
183
+ if self.audio_dataset is None:
184
+ return redirect(
185
+ url_for("humanify.challenge", return_url=request.full_path)
186
+ )
187
+
172
188
  return self._render_challenge(is_audio=True)
173
189
 
174
190
  @self.blueprint.route("/humanify/verify", methods=["POST"])
@@ -176,6 +192,9 @@ class Humanify:
176
192
  """
177
193
  Verify route.
178
194
  """
195
+ if self.image_dataset is None:
196
+ abort(404)
197
+
179
198
  return self._verify_captcha()
180
199
 
181
200
  @self.blueprint.route("/humanify/verify_audio", methods=["POST"])
@@ -183,6 +202,9 @@ class Humanify:
183
202
  """
184
203
  Verify audio route.
185
204
  """
205
+ if self.audio_dataset is None:
206
+ abort(404)
207
+
186
208
  return self._verify_audio_captcha()
187
209
 
188
210
  @self.blueprint.route("/humanify/access_denied", methods=["GET"])
@@ -198,27 +220,168 @@ class Humanify:
198
220
  {"Cache-Control": "public, max-age=15552000"},
199
221
  )
200
222
 
201
- def register_middleware(self, action: str = "challenge"):
223
+ def register_middleware(
224
+ self,
225
+ action: str = "challenge",
226
+ endpoint_patterns: Union[str, List[str], None] = None,
227
+ url_patterns: Union[str, List[str], None] = None,
228
+ exclude_patterns: Union[str, List[str], None] = None,
229
+ request_filters: Optional[Dict[str, Any]] = None,
230
+ ):
202
231
  """
203
- Register the middleware.
232
+ Register the middleware with advanced filtering options.
233
+
234
+ Args:
235
+ action: The action to take when a bot is detected ('challenge' or 'deny_access')
236
+ endpoint_patterns: Endpoint patterns to match (regex or glob patterns)
237
+ url_patterns: URL patterns to match (regex or glob patterns)
238
+ exclude_patterns: Patterns to exclude from protection (regex or glob patterns)
239
+ request_filters: Dict of request attributes and values to filter by
204
240
  """
205
-
206
241
  self.app = self.app or current_app
207
242
 
243
+ if isinstance(endpoint_patterns, str):
244
+ endpoint_patterns = [endpoint_patterns]
245
+ if isinstance(url_patterns, str):
246
+ url_patterns = [url_patterns]
247
+ if isinstance(exclude_patterns, str):
248
+ exclude_patterns = [exclude_patterns]
249
+
250
+ compiled_endpoint_patterns = (
251
+ self._compile_patterns(endpoint_patterns) if endpoint_patterns else None
252
+ )
253
+ compiled_url_patterns = (
254
+ self._compile_patterns(url_patterns) if url_patterns else None
255
+ )
256
+ compiled_exclude_patterns = (
257
+ self._compile_patterns(exclude_patterns) if exclude_patterns else None
258
+ )
259
+
208
260
  @self.app.before_request
209
261
  def before_request():
210
262
  """
211
- Before request hook.
263
+ Before request hook with advanced filtering.
212
264
  """
213
265
  if request.endpoint and request.endpoint.startswith("humanify."):
214
266
  return
215
267
 
216
- if self.is_bot:
268
+ current_endpoint = request.endpoint or ""
269
+ current_path = request.path
270
+
271
+ if compiled_exclude_patterns and self._matches_any_pattern(
272
+ current_endpoint, current_path, compiled_exclude_patterns
273
+ ):
274
+ return
275
+
276
+ patterns_specified = (
277
+ compiled_endpoint_patterns is not None
278
+ or compiled_url_patterns is not None
279
+ )
280
+
281
+ matches_endpoint = not patterns_specified or (
282
+ compiled_endpoint_patterns
283
+ and self._matches_any_pattern(
284
+ current_endpoint, None, compiled_endpoint_patterns
285
+ )
286
+ )
287
+
288
+ matches_url = not patterns_specified or (
289
+ compiled_url_patterns
290
+ and self._matches_any_pattern(None, current_path, compiled_url_patterns)
291
+ )
292
+
293
+ matches_request_filters = (
294
+ not request_filters or self._matches_request_filters(request_filters)
295
+ )
296
+
297
+ if (
298
+ (matches_endpoint or matches_url)
299
+ and matches_request_filters
300
+ and self.is_bot
301
+ ):
217
302
  if action == "challenge":
218
303
  return self.challenge()
219
304
  if action == "deny_access":
220
305
  return self.deny_access()
221
306
 
307
+ def _compile_patterns(self, patterns):
308
+ """
309
+ Compile a list of patterns into regex patterns.
310
+ Handles glob patterns like * and ? by converting them to regex.
311
+ """
312
+ compiled = []
313
+ for pattern in patterns:
314
+ if pattern is None:
315
+ continue
316
+
317
+ if "*" in pattern or "?" in pattern:
318
+ regex_pattern = fnmatch.translate(pattern)
319
+ compiled.append(re.compile(regex_pattern))
320
+ else:
321
+ try:
322
+ compiled.append(re.compile(pattern))
323
+ except re.error:
324
+ compiled.append(re.compile(re.escape(pattern)))
325
+
326
+ return compiled
327
+
328
+ def _matches_any_pattern(
329
+ self,
330
+ endpoint: Optional[str],
331
+ path: Optional[str],
332
+ compiled_patterns: List[Pattern],
333
+ ):
334
+ """
335
+ Check if the current endpoint or path matches any of the compiled patterns.
336
+ """
337
+ for pattern in compiled_patterns:
338
+ if endpoint is not None and pattern.search(endpoint):
339
+ return True
340
+ if path is not None and pattern.search(path):
341
+ return True
342
+ return False
343
+
344
+ def _matches_request_filters(self, request_filters: Dict[str, Any]) -> bool:
345
+ """
346
+ Check if the current request matches all the specified filters.
347
+ Filters can target any attribute of the request object or its nested properties.
348
+ """
349
+ for key, value in request_filters.items():
350
+ parts = key.split(".")
351
+ obj = request
352
+
353
+ for part in parts[:-1]:
354
+ if hasattr(obj, part):
355
+ obj = getattr(obj, part)
356
+ elif isinstance(obj, dict) and part in obj:
357
+ obj = obj[part]
358
+ else:
359
+ return False
360
+
361
+ final_attr = parts[-1]
362
+
363
+ if hasattr(obj, final_attr):
364
+ attr_value = getattr(obj, final_attr)
365
+ elif isinstance(obj, dict) and final_attr in obj:
366
+ attr_value = obj[final_attr]
367
+ else:
368
+ return False
369
+
370
+ if isinstance(value, str) and value.startswith("regex:"):
371
+ regex_pattern = value[6:]
372
+ try:
373
+ if not re.search(regex_pattern, str(attr_value)):
374
+ return False
375
+ except (re.error, TypeError):
376
+ return False
377
+ elif isinstance(value, list):
378
+ if attr_value not in value:
379
+ return False
380
+ elif attr_value != value:
381
+ return False
382
+
383
+ return True
384
+
222
385
  @property
223
386
  def client_ip(self) -> Optional[str]:
224
387
  """Get the client IP address."""
@@ -310,7 +473,7 @@ class Humanify:
310
473
  num_correct=captcha_config["num_correct"],
311
474
  num_images=captcha_config["num_images"],
312
475
  preview_image=use_preview_image,
313
- dataset_name=self.captcha_dataset,
476
+ dataset_name=self.image_dataset,
314
477
  )
315
478
 
316
479
  if not images_bytes:
@@ -319,16 +482,15 @@ class Humanify:
319
482
  processed_images = []
320
483
  for i, img_bytes in enumerate(images_bytes):
321
484
  try:
322
- hardness = random.randint(
323
- captcha_config["hardness_range"][0],
324
- captcha_config["hardness_range"][1],
325
- )
326
- distorted = manipulate_image_bytes(
485
+ distorted_img_bytes = manipulate_image_bytes(
327
486
  img_bytes,
328
487
  is_small=not (i == 0 and use_preview_image),
329
- hardness=hardness,
488
+ hardness=random.randint(
489
+ captcha_config["hardness_range"][0],
490
+ captcha_config["hardness_range"][1],
491
+ ),
330
492
  )
331
- processed_images.append(image_bytes_to_data_url(distorted))
493
+ processed_images.append(image_bytes_to_data_url(distorted_img_bytes))
332
494
  except Exception as e:
333
495
  current_app.logger.error(f"Error processing image: {e}")
334
496
  processed_images.append(
@@ -359,7 +521,7 @@ class Humanify:
359
521
  captcha_data=captcha_data,
360
522
  return_url=return_url or "/",
361
523
  error=error,
362
- audio_challenge_available=True,
524
+ audio_challenge_available=self.audio_dataset is not None,
363
525
  ),
364
526
  mimetype="text/html",
365
527
  )
@@ -400,7 +562,7 @@ class Humanify:
400
562
  captcha_data=captcha_data,
401
563
  return_url=return_url or "/",
402
564
  error=error,
403
- image_challenge_available=True,
565
+ image_challenge_available=self.image_dataset is not None,
404
566
  ),
405
567
  mimetype="text/html",
406
568
  )
@@ -204,7 +204,9 @@ class MemoryServer:
204
204
  return False
205
205
 
206
206
  def load_captcha_datasets(
207
- self, image_dataset: str = "animals", audio_dataset: Optional[str] = None
207
+ self,
208
+ image_dataset: Optional[str] = None,
209
+ audio_dataset: Optional[str] = None,
208
210
  ) -> bool:
209
211
  """Load captcha datasets into memory."""
210
212
  try:
@@ -819,8 +821,8 @@ class MemoryClient:
819
821
  def ensure_server_running(
820
822
  port: int = 9876,
821
823
  data_path: Optional[str] = None,
822
- image_dataset: str = "animals",
823
- audio_dataset: str = "characters",
824
+ image_dataset: Optional[str] = None,
825
+ audio_dataset: Optional[str] = None,
824
826
  ) -> None:
825
827
  """Ensure that the memory server is running."""
826
828
  if data_path is None:
@@ -251,6 +251,8 @@ def manipulate_image_bytes(
251
251
  """Manipulates an image represented by bytes to create a distorted version."""
252
252
  # pylint: disable=no-member
253
253
 
254
+ hardness = min(max(1, hardness), 4)
255
+
254
256
  img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
255
257
  if img is None:
256
258
  logger.error("Image data could not be decoded by OpenCV")
@@ -259,25 +261,85 @@ def manipulate_image_bytes(
259
261
  size = 100 if is_small else 200
260
262
  img = cv2.resize(img, (size, size), interpolation=cv2.INTER_LINEAR)
261
263
 
262
- if hardness > 3:
263
- num_dots = np.random.randint(20, 100) * (hardness - 3)
264
- dot_coords = np.random.randint(0, [size, size], size=(num_dots, 2))
265
- colors = np.random.randint(0, 256, size=(num_dots, 3))
264
+ mask_pattern = np.zeros((size, size, 3), dtype=np.uint8)
265
+
266
+ grid_size = max(8, 16 - hardness * 2)
267
+ for i in range(0, size, grid_size):
268
+ thickness = 1
269
+ cv2.line(mask_pattern, (i, 0), (i, size), (2, 2, 2), thickness)
270
+ cv2.line(mask_pattern, (0, i), (size, i), (2, 2, 2), thickness)
266
271
 
267
- for (x, y), color in zip(dot_coords, colors):
268
- img[y, x] = color
272
+ mask_opacity = min(0.06 + hardness * 0.03, 0.18)
273
+ img = cv2.addWeighted(img, 1 - mask_opacity, mask_pattern, mask_opacity, 0)
269
274
 
270
- num_lines = np.random.randint(20, 100) * (hardness - 3)
271
- start_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
272
- end_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
273
- colors = np.random.randint(0, 256, size=(num_lines, 3))
275
+ noise_max = max(1, 1 + hardness // 2)
276
+ noise_pattern = np.random.randint(
277
+ 0, noise_max, size=(size, size, 3), dtype=np.uint8
278
+ )
279
+ img = cv2.add(img, noise_pattern)
280
+
281
+ num_dots = np.random.randint(5 + 5 * hardness, 10 + 10 * hardness + 1)
282
+ dot_coords = np.random.randint(0, [size, size], size=(num_dots, 2))
283
+
284
+ dot_intensity = 0.05 + hardness * 0.05
285
+ rand_max = max(1, 10 * hardness)
286
+ colors = np.random.randint(0, rand_max, size=(num_dots, 3)) + np.array(
287
+ [img[coord[1], coord[0]] for coord in dot_coords]
288
+ ) * (1 - dot_intensity)
289
+ colors = np.clip(colors, 0, 255).astype(np.uint8)
290
+
291
+ for (x, y), color in zip(dot_coords, colors):
292
+ img[y, x] = color
293
+
294
+ num_lines = np.random.randint(2 * hardness, 5 * hardness + 1)
295
+ start_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
296
+ end_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
297
+
298
+ line_intensity = max(4, 3 * hardness)
299
+ colors = np.random.randint(3, line_intensity, size=(num_lines, 3))
300
+
301
+ for (start, end), color in zip(zip(start_coords, end_coords), colors):
302
+ cv2.line(img, tuple(start), tuple(end), color.tolist(), 1)
303
+
304
+ for _ in range(hardness):
305
+ x = np.random.randint(0, size)
306
+ y = np.random.randint(0, size)
307
+ length = np.random.randint(5 + 3 * hardness, 10 + 5 * hardness + 1)
308
+ angle = np.random.randint(0, 360)
309
+ text_max = max(3, 2 + hardness)
310
+ text_color = np.random.randint(1, text_max, 3).tolist()
311
+
312
+ end_x = int(x + length * np.cos(np.radians(angle)))
313
+ end_y = int(y + length * np.sin(np.radians(angle)))
314
+ cv2.line(img, (x, y), (end_x, end_y), text_color, 1)
315
+
316
+ for _ in range(1 + hardness // 2):
317
+ patch_size = np.random.randint(4 + hardness, 6 + 3 * hardness + 1)
318
+ x = np.random.randint(0, size - patch_size)
319
+ y = np.random.randint(0, size - patch_size)
320
+
321
+ patch = np.zeros((patch_size, patch_size, 3), dtype=np.uint8)
322
+ for i in range(0, patch_size, 2):
323
+ for j in range(0, patch_size, 2):
324
+ if (i + j) % 4 == 0:
325
+ patch_color_max = max(2, 1 + hardness)
326
+ patch[i : i + 2, j : j + 2] = [
327
+ np.random.randint(1, patch_color_max)
328
+ ] * 3
329
+
330
+ patch_opacity = 0.03 + 0.02 * hardness
331
+ roi = img[y : y + patch_size, x : x + patch_size]
332
+ img[y : y + patch_size, x : x + patch_size] = cv2.addWeighted(
333
+ roi, 1 - patch_opacity, patch, patch_opacity, 0
334
+ )
274
335
 
275
- for (start, end), color in zip(zip(start_coords, end_coords), colors):
276
- cv2.line(img, tuple(start), tuple(end), color.tolist(), 1)
336
+ max_shift = hardness
337
+ x_shifts = np.random.randint(-max_shift, max_shift + 1, size=(size, size))
338
+ y_shifts = np.random.randint(-max_shift, max_shift + 1, size=(size, size))
277
339
 
278
- max_shift = max(3, hardness + 1)
279
- x_shifts = np.random.randint(-max(2, hardness + 4), max_shift, size=(size, size))
280
- y_shifts = np.random.randint(-max(1, hardness + 4), max_shift, size=(size, size))
340
+ saturation_factor = 1 + hardness * 0.05
341
+ value_factor = 1 - hardness * 0.03
342
+ blur_factor = hardness * 0.05
281
343
 
282
344
  map_x, map_y = np.meshgrid(np.arange(size), np.arange(size))
283
345
  map_x = (map_x + x_shifts) % size
@@ -289,14 +351,18 @@ def manipulate_image_bytes(
289
351
  shifted_img_hsv = cv2.cvtColor(shifted_img, cv2.COLOR_BGR2HSV)
290
352
 
291
353
  shifted_img_hsv[..., 1] = np.clip(
292
- shifted_img_hsv[..., 1] * (1 + hardness * 0.12), 0, 255
293
- )
294
- shifted_img_hsv[..., 2] = np.clip(
295
- shifted_img_hsv[..., 2] * (1 - hardness * 0.09), 0, 255
354
+ shifted_img_hsv[..., 1] * saturation_factor, 0, 255
296
355
  )
356
+ shifted_img_hsv[..., 2] = np.clip(shifted_img_hsv[..., 2] * value_factor, 0, 255)
297
357
 
298
358
  shifted_img = cv2.cvtColor(shifted_img_hsv, cv2.COLOR_HSV2BGR)
299
- shifted_img = cv2.GaussianBlur(shifted_img, (5, 5), hardness * 0.1)
359
+ shifted_img = cv2.GaussianBlur(shifted_img, (5, 5), blur_factor)
360
+
361
+ noise_high = max(1, 1 + hardness // 3)
362
+ high_freq_noise = np.random.randint(
363
+ 0, noise_high, size=shifted_img.shape, dtype=np.uint8
364
+ )
365
+ shifted_img = cv2.add(shifted_img, high_freq_noise)
300
366
 
301
367
  _, output_bytes = cv2.imencode(".png", shifted_img)
302
368
  if not _:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flask-Humanify"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Protect against bots and DDoS attacks"
5
5
  readme = "README.md"
6
6
  authors = [
File without changes
File without changes
File without changes