dotcha 0.1.0__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.
- dotcha-0.1.0/LICENSE +24 -0
- dotcha-0.1.0/PKG-INFO +106 -0
- dotcha-0.1.0/README.md +89 -0
- dotcha-0.1.0/dotcha/__init__.py +4 -0
- dotcha-0.1.0/dotcha/generator.py +240 -0
- dotcha-0.1.0/dotcha/shapes.py +83 -0
- dotcha-0.1.0/dotcha/theme.py +53 -0
- dotcha-0.1.0/dotcha.egg-info/PKG-INFO +106 -0
- dotcha-0.1.0/dotcha.egg-info/SOURCES.txt +12 -0
- dotcha-0.1.0/dotcha.egg-info/dependency_links.txt +1 -0
- dotcha-0.1.0/dotcha.egg-info/requires.txt +1 -0
- dotcha-0.1.0/dotcha.egg-info/top_level.txt +1 -0
- dotcha-0.1.0/pyproject.toml +29 -0
- dotcha-0.1.0/setup.cfg +4 -0
dotcha-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
dotcha-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotcha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gestalt Illusion Captcha Generator for Python
|
|
5
|
+
Author: Dotcha Authors
|
|
6
|
+
License-Expression: Unlicense
|
|
7
|
+
Project-URL: Homepage, https://github.com/trombalny/dotcha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: Pillow>=9.0.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Dotcha 🌀
|
|
19
|
+
|
|
20
|
+
**Dotcha** is a high-performance Python library for generating captchas based on the **Gestalt Illusion** principle. It creates visual patterns from thousands of geometric shapes that are intuitive for humans but highly resistant to automated OCR and AI solvers.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
- 🎨 **Gestalt Illusion**: Pure geometric rendering for maximum machine-learning resistance.
|
|
24
|
+
- ⚡ **High Performance**: Optimized rendering (~30ms per PNG).
|
|
25
|
+
- 🔄 **Animated GIFs**: "Temporal Gestalt" effect where text is reconstruction through motion.
|
|
26
|
+
- 🛡️ **Fuzzy Validation**: Intelligent answer verification with Levenshtein distance.
|
|
27
|
+
- 🌓 **Universal Themes**: Light, Dark, and fully customizable color schemas.
|
|
28
|
+
- 📉 **Scalable Difficulty**: Dynamic calibration from clear patterns to chaotic noise.
|
|
29
|
+
- 🤖 **Async Ready**: Native `asyncio` support for web frameworks and bots.
|
|
30
|
+
|
|
31
|
+
## Visual Examples
|
|
32
|
+
|
|
33
|
+
| Light Theme (EASY) | Dark Theme (MEDIUM) | Animated (Temporal Gestalt) |
|
|
34
|
+
|:---:|:---:|:---:|
|
|
35
|
+
|  |  |  |
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install Pillow
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Synchronous PNG
|
|
46
|
+
```python
|
|
47
|
+
from dotcha import CaptchaGenerator, Theme
|
|
48
|
+
|
|
49
|
+
gen = CaptchaGenerator(theme=Theme.LIGHT)
|
|
50
|
+
text, buffer = gen.generate()
|
|
51
|
+
|
|
52
|
+
with open("captcha.png", "wb") as f:
|
|
53
|
+
f.write(buffer.read())
|
|
54
|
+
print(f"Generated captcha: {text}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Asynchronous GIF
|
|
58
|
+
```python
|
|
59
|
+
from dotcha import CaptchaGenerator, Theme, Difficulty
|
|
60
|
+
|
|
61
|
+
async def send_captcha():
|
|
62
|
+
gen = CaptchaGenerator(theme=Theme.DARK, difficulty=Difficulty.HARD)
|
|
63
|
+
text, buffer = await gen.agenerate_gif(frames=12)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> 💡 See [examples/bot_demo.py](examples/bot_demo.py) for a complete Telegram bot integration.
|
|
67
|
+
|
|
68
|
+
### Fuzzy Verification
|
|
69
|
+
```python
|
|
70
|
+
from dotcha import CaptchaGenerator
|
|
71
|
+
|
|
72
|
+
user_input = "ABCDE"
|
|
73
|
+
actual = "ABCD1"
|
|
74
|
+
|
|
75
|
+
# Accepts answer with 1 char distance
|
|
76
|
+
is_valid, distance = CaptchaGenerator.check_answer(user_input, actual, fuzzy_tolerance=1)
|
|
77
|
+
if is_valid:
|
|
78
|
+
print(f"Passed! Distance: {distance}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Why Dotcha?
|
|
82
|
+
- **Pattern-Based Security**: While a human brain naturally connects scattered dots into characters, standard OCR algorithms perceive them as disconnected noise.
|
|
83
|
+
- **Temporal Signal**: The GIF format utilizes "Temporal Sparsity". A static frame is unreadable, but the human eye integrates movement into a clear signal.
|
|
84
|
+
- **Zero Disk Footprint**: Captchas are generated directly into byte buffers, making it ideal for high-concurrency environments.
|
|
85
|
+
- **Stable & Lightweight**: Explicit resource management and minimal dependencies.
|
|
86
|
+
|
|
87
|
+
## Performance
|
|
88
|
+
|
|
89
|
+
### Static Images (PNG)
|
|
90
|
+
| Difficulty | Time | Shapes | Description |
|
|
91
|
+
|------------|------|--------|-------------|
|
|
92
|
+
| **EASY** | ~0.05s | 8000 | High density, very clear for humans. |
|
|
93
|
+
| **MEDIUM** | ~0.03s | 5000 | Balanced density and noise. |
|
|
94
|
+
| **HARD** | ~0.02s | 3500 | Sparse text, higher background chaos. |
|
|
95
|
+
|
|
96
|
+
### Animated Captchas (GIF)
|
|
97
|
+
| Difficulty | Time (12 frames) | Security Level |
|
|
98
|
+
|------------|-----------------|----------------|
|
|
99
|
+
| **EASY** | ~0.51s | Maximum (High signal integration) |
|
|
100
|
+
| **MEDIUM** | ~0.38s | Standard |
|
|
101
|
+
| **HARD** | ~0.32s | Extreme (Sparse signal in motion) |
|
|
102
|
+
|
|
103
|
+
> *Note: Timings are based on a standard modern CPU. Generation is offloaded to background threads to keep your bot responsive.*
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
This project is released into the public domain under the [Unlicense](LICENSE).
|
dotcha-0.1.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Dotcha 🌀
|
|
2
|
+
|
|
3
|
+
**Dotcha** is a high-performance Python library for generating captchas based on the **Gestalt Illusion** principle. It creates visual patterns from thousands of geometric shapes that are intuitive for humans but highly resistant to automated OCR and AI solvers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- 🎨 **Gestalt Illusion**: Pure geometric rendering for maximum machine-learning resistance.
|
|
7
|
+
- ⚡ **High Performance**: Optimized rendering (~30ms per PNG).
|
|
8
|
+
- 🔄 **Animated GIFs**: "Temporal Gestalt" effect where text is reconstruction through motion.
|
|
9
|
+
- 🛡️ **Fuzzy Validation**: Intelligent answer verification with Levenshtein distance.
|
|
10
|
+
- 🌓 **Universal Themes**: Light, Dark, and fully customizable color schemas.
|
|
11
|
+
- 📉 **Scalable Difficulty**: Dynamic calibration from clear patterns to chaotic noise.
|
|
12
|
+
- 🤖 **Async Ready**: Native `asyncio` support for web frameworks and bots.
|
|
13
|
+
|
|
14
|
+
## Visual Examples
|
|
15
|
+
|
|
16
|
+
| Light Theme (EASY) | Dark Theme (MEDIUM) | Animated (Temporal Gestalt) |
|
|
17
|
+
|:---:|:---:|:---:|
|
|
18
|
+
|  |  |  |
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install Pillow
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Synchronous PNG
|
|
29
|
+
```python
|
|
30
|
+
from dotcha import CaptchaGenerator, Theme
|
|
31
|
+
|
|
32
|
+
gen = CaptchaGenerator(theme=Theme.LIGHT)
|
|
33
|
+
text, buffer = gen.generate()
|
|
34
|
+
|
|
35
|
+
with open("captcha.png", "wb") as f:
|
|
36
|
+
f.write(buffer.read())
|
|
37
|
+
print(f"Generated captcha: {text}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Asynchronous GIF
|
|
41
|
+
```python
|
|
42
|
+
from dotcha import CaptchaGenerator, Theme, Difficulty
|
|
43
|
+
|
|
44
|
+
async def send_captcha():
|
|
45
|
+
gen = CaptchaGenerator(theme=Theme.DARK, difficulty=Difficulty.HARD)
|
|
46
|
+
text, buffer = await gen.agenerate_gif(frames=12)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> 💡 See [examples/bot_demo.py](examples/bot_demo.py) for a complete Telegram bot integration.
|
|
50
|
+
|
|
51
|
+
### Fuzzy Verification
|
|
52
|
+
```python
|
|
53
|
+
from dotcha import CaptchaGenerator
|
|
54
|
+
|
|
55
|
+
user_input = "ABCDE"
|
|
56
|
+
actual = "ABCD1"
|
|
57
|
+
|
|
58
|
+
# Accepts answer with 1 char distance
|
|
59
|
+
is_valid, distance = CaptchaGenerator.check_answer(user_input, actual, fuzzy_tolerance=1)
|
|
60
|
+
if is_valid:
|
|
61
|
+
print(f"Passed! Distance: {distance}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Why Dotcha?
|
|
65
|
+
- **Pattern-Based Security**: While a human brain naturally connects scattered dots into characters, standard OCR algorithms perceive them as disconnected noise.
|
|
66
|
+
- **Temporal Signal**: The GIF format utilizes "Temporal Sparsity". A static frame is unreadable, but the human eye integrates movement into a clear signal.
|
|
67
|
+
- **Zero Disk Footprint**: Captchas are generated directly into byte buffers, making it ideal for high-concurrency environments.
|
|
68
|
+
- **Stable & Lightweight**: Explicit resource management and minimal dependencies.
|
|
69
|
+
|
|
70
|
+
## Performance
|
|
71
|
+
|
|
72
|
+
### Static Images (PNG)
|
|
73
|
+
| Difficulty | Time | Shapes | Description |
|
|
74
|
+
|------------|------|--------|-------------|
|
|
75
|
+
| **EASY** | ~0.05s | 8000 | High density, very clear for humans. |
|
|
76
|
+
| **MEDIUM** | ~0.03s | 5000 | Balanced density and noise. |
|
|
77
|
+
| **HARD** | ~0.02s | 3500 | Sparse text, higher background chaos. |
|
|
78
|
+
|
|
79
|
+
### Animated Captchas (GIF)
|
|
80
|
+
| Difficulty | Time (12 frames) | Security Level |
|
|
81
|
+
|------------|-----------------|----------------|
|
|
82
|
+
| **EASY** | ~0.51s | Maximum (High signal integration) |
|
|
83
|
+
| **MEDIUM** | ~0.38s | Standard |
|
|
84
|
+
| **HARD** | ~0.32s | Extreme (Sparse signal in motion) |
|
|
85
|
+
|
|
86
|
+
> *Note: Timings are based on a standard modern CPU. Generation is offloaded to background threads to keep your bot responsive.*
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
This project is released into the public domain under the [Unlicense](LICENSE).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Tuple, Union, Optional, List
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
9
|
+
|
|
10
|
+
from .theme import ColorSchema, Theme, Difficulty, LIGHT_SCHEMA, DARK_SCHEMA
|
|
11
|
+
from .shapes import Shape, Circle, Triangle, Line, Square, Star
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CaptchaGenerator:
|
|
15
|
+
"""
|
|
16
|
+
Advanced Gestalt Illusion Captcha Generator.
|
|
17
|
+
Supports Distortion, GIFs, and Difficulty Levels.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
DEFAULT_WIDTH = 400
|
|
21
|
+
DEFAULT_HEIGHT = 200
|
|
22
|
+
|
|
23
|
+
DIFFICULTY_CONFIG = {
|
|
24
|
+
Difficulty.EASY: {"shapes": 8000, "text_size_range": (3, 4), "bg_size_range": (1, 2)},
|
|
25
|
+
Difficulty.MEDIUM: {"shapes": 5000, "text_size_range": (3, 5), "bg_size_range": (2, 4)},
|
|
26
|
+
Difficulty.HARD: {"shapes": 3500, "text_size_range": (2, 4), "bg_size_range": (3, 6)},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
theme: Union[str, Theme, ColorSchema] = Theme.LIGHT,
|
|
32
|
+
difficulty: Difficulty = Difficulty.MEDIUM,
|
|
33
|
+
width: int = DEFAULT_WIDTH,
|
|
34
|
+
height: int = DEFAULT_HEIGHT,
|
|
35
|
+
char_length: int = 5,
|
|
36
|
+
font_size: Optional[int] = None
|
|
37
|
+
):
|
|
38
|
+
self.width = width
|
|
39
|
+
self.height = height
|
|
40
|
+
self.difficulty = difficulty
|
|
41
|
+
self.char_length = char_length
|
|
42
|
+
self.schema = self._get_schema(theme)
|
|
43
|
+
|
|
44
|
+
# Adaptive font size if not provided (approx 40% of height)
|
|
45
|
+
self.font_size = font_size or int(height * 0.45)
|
|
46
|
+
self._font = self._load_font(self.font_size)
|
|
47
|
+
self._config = self.DIFFICULTY_CONFIG[difficulty]
|
|
48
|
+
|
|
49
|
+
def _get_schema(self, theme: Union[str, Theme, ColorSchema]) -> ColorSchema:
|
|
50
|
+
if isinstance(theme, ColorSchema):
|
|
51
|
+
return theme
|
|
52
|
+
|
|
53
|
+
theme_val = theme.value if isinstance(theme, Theme) else theme
|
|
54
|
+
if theme_val == Theme.DARK.value:
|
|
55
|
+
return DARK_SCHEMA
|
|
56
|
+
|
|
57
|
+
return LIGHT_SCHEMA
|
|
58
|
+
|
|
59
|
+
def _load_font(self, size: int) -> ImageFont.FreeTypeFont:
|
|
60
|
+
# Support multiple common fonts or fallback
|
|
61
|
+
font_names = ["arial.ttf", "DejaVuSans.ttf", "Verdana.ttf", "tahoma.ttf"]
|
|
62
|
+
for name in font_names:
|
|
63
|
+
try:
|
|
64
|
+
return ImageFont.truetype(name, size)
|
|
65
|
+
except OSError:
|
|
66
|
+
continue
|
|
67
|
+
return ImageFont.load_default()
|
|
68
|
+
|
|
69
|
+
def _generate_text(self, length: int = 5) -> str:
|
|
70
|
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
|
|
71
|
+
|
|
72
|
+
def _apply_distortion(self, image: Image.Image) -> Image.Image:
|
|
73
|
+
"""Apply mesh distortion to the mask image."""
|
|
74
|
+
# Simple waviness
|
|
75
|
+
width, height = image.size
|
|
76
|
+
# Create a copy as we are working with pixel data
|
|
77
|
+
distorted = image.copy()
|
|
78
|
+
# In practice, ImageOps.deform or custom transform is better
|
|
79
|
+
# For POC, we'll use a simple affine transform for slight slant/shear
|
|
80
|
+
shear_factor = random.uniform(-0.2, 0.2)
|
|
81
|
+
distorted = distorted.transform(
|
|
82
|
+
(width, height),
|
|
83
|
+
Image.AFFINE,
|
|
84
|
+
(1, shear_factor, 0, 0, 1, 0),
|
|
85
|
+
resample=Image.BICUBIC
|
|
86
|
+
)
|
|
87
|
+
return distorted
|
|
88
|
+
|
|
89
|
+
def _add_glitches(self, draw: ImageDraw.ImageDraw, mask_pixels: any):
|
|
90
|
+
"""Add lines that cross characters to confuse OCR."""
|
|
91
|
+
for _ in range(random.randint(3, 7)):
|
|
92
|
+
x1 = random.randint(0, self.width)
|
|
93
|
+
y1 = random.randint(0, self.height)
|
|
94
|
+
x2 = x1 + random.randint(-50, 50)
|
|
95
|
+
y2 = y1 + random.randint(-30, 30)
|
|
96
|
+
|
|
97
|
+
# Draw on mask if we wanted to affect shape logic,
|
|
98
|
+
# but here we just draw some random shapes on the canvas later
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def _render(self, frames: int = 1) -> Tuple[str, Union[io.BytesIO, List[io.BytesIO]]]:
|
|
102
|
+
"""
|
|
103
|
+
Rendering logic. If frames > 1, returns frames for GIF.
|
|
104
|
+
"""
|
|
105
|
+
text = self._generate_text(length=self.char_length)
|
|
106
|
+
|
|
107
|
+
# 1. Create mask with distortion
|
|
108
|
+
base_mask = Image.new('L', (self.width, self.height), 0)
|
|
109
|
+
mask_draw = ImageDraw.Draw(base_mask)
|
|
110
|
+
|
|
111
|
+
text_bbox = mask_draw.textbbox((0, 0), text, font=self._font)
|
|
112
|
+
text_width = text_bbox[2] - text_bbox[0]
|
|
113
|
+
text_height = text_bbox[3] - text_bbox[1]
|
|
114
|
+
pos = ((self.width - text_width) // 2, (self.height - text_height) // 2)
|
|
115
|
+
mask_draw.text(pos, text, font=self._font, fill=255)
|
|
116
|
+
|
|
117
|
+
mask = self._apply_distortion(base_mask)
|
|
118
|
+
mask_pixels = mask.load()
|
|
119
|
+
|
|
120
|
+
# 2. Render logic
|
|
121
|
+
def draw_frame(frame_idx: int) -> Image.Image:
|
|
122
|
+
canvas = Image.new('RGB', (self.width, self.height), self.schema.background)
|
|
123
|
+
draw = ImageDraw.Draw(canvas, 'RGBA')
|
|
124
|
+
shape_types = [Circle, Triangle, Line, Square, Star]
|
|
125
|
+
|
|
126
|
+
shapes_to_draw = self._config["shapes"]
|
|
127
|
+
|
|
128
|
+
for i in range(shapes_to_draw):
|
|
129
|
+
# For GIF animation, background shapes drift, text shapes stay fixed
|
|
130
|
+
jitter_x = 0
|
|
131
|
+
jitter_y = 0
|
|
132
|
+
|
|
133
|
+
x = random.randint(0, self.width - 1)
|
|
134
|
+
y = random.randint(0, self.height - 1)
|
|
135
|
+
|
|
136
|
+
mask_val = mask_pixels[x, y]
|
|
137
|
+
is_text = mask_val > 128
|
|
138
|
+
|
|
139
|
+
if is_text and frames > 1:
|
|
140
|
+
# Temporal Sparsity: Only show ~40% of the text in any single frame
|
|
141
|
+
# The human brain integrates this over time, but OCR sees a broken pattern
|
|
142
|
+
if random.random() > 0.4:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if not is_text and frames > 1:
|
|
146
|
+
# Drift background shapes based on frame index with some randomness
|
|
147
|
+
x = (x + frame_idx * random.randint(3, 7)) % self.width
|
|
148
|
+
|
|
149
|
+
color_list = self.schema.text_colors if is_text else self.schema.background_colors
|
|
150
|
+
color = random.choice(color_list)
|
|
151
|
+
|
|
152
|
+
shape_cls = random.choice(shape_types)
|
|
153
|
+
if is_text:
|
|
154
|
+
size = random.randint(*self._config["text_size_range"])
|
|
155
|
+
else:
|
|
156
|
+
size = random.randint(*self._config["bg_size_range"])
|
|
157
|
+
|
|
158
|
+
shape = shape_cls(x, y, size)
|
|
159
|
+
shape.draw(draw, color)
|
|
160
|
+
|
|
161
|
+
return canvas
|
|
162
|
+
|
|
163
|
+
if frames == 1:
|
|
164
|
+
canvas = draw_frame(0)
|
|
165
|
+
buffer = io.BytesIO()
|
|
166
|
+
canvas.save(buffer, format='PNG')
|
|
167
|
+
buffer.seek(0)
|
|
168
|
+
|
|
169
|
+
mask.close()
|
|
170
|
+
base_mask.close()
|
|
171
|
+
canvas.close()
|
|
172
|
+
return text, buffer
|
|
173
|
+
|
|
174
|
+
# Multiple frames for GIF
|
|
175
|
+
rendered_frames = []
|
|
176
|
+
for f in range(frames):
|
|
177
|
+
rendered_frames.append(draw_frame(f))
|
|
178
|
+
|
|
179
|
+
buffer = io.BytesIO()
|
|
180
|
+
rendered_frames[0].save(
|
|
181
|
+
buffer,
|
|
182
|
+
format='GIF',
|
|
183
|
+
save_all=True,
|
|
184
|
+
append_images=rendered_frames[1:],
|
|
185
|
+
duration=100,
|
|
186
|
+
loop=0
|
|
187
|
+
)
|
|
188
|
+
buffer.seek(0)
|
|
189
|
+
|
|
190
|
+
# Cleanup
|
|
191
|
+
mask.close()
|
|
192
|
+
base_mask.close()
|
|
193
|
+
for f in rendered_frames:
|
|
194
|
+
f.close()
|
|
195
|
+
|
|
196
|
+
return text, buffer
|
|
197
|
+
|
|
198
|
+
def generate(self) -> Tuple[str, io.BytesIO]:
|
|
199
|
+
return self._render(frames=1)
|
|
200
|
+
|
|
201
|
+
async def agenerate(self) -> Tuple[str, io.BytesIO]:
|
|
202
|
+
return await asyncio.to_thread(self._render, frames=1)
|
|
203
|
+
|
|
204
|
+
def generate_gif(self, frames: int = 8) -> Tuple[str, io.BytesIO]:
|
|
205
|
+
return self._render(frames=frames)
|
|
206
|
+
|
|
207
|
+
async def agenerate_gif(self, frames: int = 8) -> Tuple[str, io.BytesIO]:
|
|
208
|
+
return await asyncio.to_thread(self._render, frames=frames)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def check_answer(user_input: str, actual: str, fuzzy_tolerance: int = 0) -> Tuple[bool, int]:
|
|
212
|
+
"""
|
|
213
|
+
Validates the user's answer.
|
|
214
|
+
Returns Tuple[is_correct, distance].
|
|
215
|
+
Distance is Levenshtein distance.
|
|
216
|
+
"""
|
|
217
|
+
s1 = user_input.strip().upper()
|
|
218
|
+
s2 = actual.strip().upper()
|
|
219
|
+
|
|
220
|
+
# Calculate Levenshtein Distance
|
|
221
|
+
if len(s1) < len(s2):
|
|
222
|
+
return CaptchaGenerator.check_answer(s2, s1, fuzzy_tolerance)[0:2]
|
|
223
|
+
|
|
224
|
+
if len(s2) == 0:
|
|
225
|
+
return s1 == s2, len(s1)
|
|
226
|
+
|
|
227
|
+
previous_row = range(len(s2) + 1)
|
|
228
|
+
for i, c1 in enumerate(s1):
|
|
229
|
+
current_row = [i + 1]
|
|
230
|
+
for j, c2 in enumerate(s2):
|
|
231
|
+
insertions = previous_row[j + 1] + 1
|
|
232
|
+
deletions = current_row[j] + 1
|
|
233
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
234
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
235
|
+
previous_row = current_row
|
|
236
|
+
|
|
237
|
+
distance = previous_row[-1]
|
|
238
|
+
is_correct = distance <= fuzzy_tolerance
|
|
239
|
+
|
|
240
|
+
return is_correct, distance
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Tuple, Union
|
|
4
|
+
from PIL import ImageDraw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Shape(ABC):
|
|
8
|
+
"""Base class for all geometric shapes used in the captcha."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, x: int, y: int, size: int):
|
|
11
|
+
self.x = x
|
|
12
|
+
self.y = y
|
|
13
|
+
self.size = size
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def center(self) -> Tuple[int, int]:
|
|
17
|
+
return (self.x, self.y)
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
21
|
+
"""Draw the shape on the given PIL ImageDraw object."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Circle(Shape):
|
|
26
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
27
|
+
left_up = (self.x - self.size, self.y - self.size)
|
|
28
|
+
right_down = (self.x + self.size, self.y + self.size)
|
|
29
|
+
draw.ellipse([left_up, right_down], fill=color)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Triangle(Shape):
|
|
33
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
34
|
+
points = [
|
|
35
|
+
(self.x, self.y - self.size),
|
|
36
|
+
(self.x - self.size, self.y + self.size),
|
|
37
|
+
(self.x + self.size, self.y + self.size),
|
|
38
|
+
]
|
|
39
|
+
draw.polygon(points, fill=color)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Line(Shape):
|
|
43
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
44
|
+
angle = random.uniform(0, 3.14159)
|
|
45
|
+
dx = int(self.size * 1.5 * (1 if random.random() > 0.5 else -1))
|
|
46
|
+
dy = int(self.size * 1.5 * (1 if random.random() > 0.5 else -1))
|
|
47
|
+
|
|
48
|
+
start = (self.x - dx, self.y - dy)
|
|
49
|
+
end = (self.x + dx, self.y + dy)
|
|
50
|
+
draw.line([start, end], fill=color, width=max(1, self.size // 3))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Square(Shape):
|
|
54
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
55
|
+
left_up = (self.x - self.size, self.y - self.size)
|
|
56
|
+
right_down = (self.x + self.size, self.y + self.size)
|
|
57
|
+
draw.rectangle([left_up, right_down], fill=color)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Star(Shape):
|
|
61
|
+
def draw(self, draw: ImageDraw.ImageDraw, color: Union[str, Tuple[int, ...]]) -> None:
|
|
62
|
+
# Simple 5-point star
|
|
63
|
+
points = []
|
|
64
|
+
for i in range(10):
|
|
65
|
+
r = self.size if i % 2 == 0 else self.size // 2
|
|
66
|
+
angle = i * 36 * 0.0174533 # degrees to radians
|
|
67
|
+
px = self.x + int(r * random.random() * 0.5 + r * 0.5) # some jitter
|
|
68
|
+
px = self.x + int(r * 0.8 * (1 if i%2==0 else 0.5) * (random.uniform(0.8, 1.2))) # jitter
|
|
69
|
+
# Let's do a simpler star
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Better simple 4-point star (cross-like)
|
|
73
|
+
points = [
|
|
74
|
+
(self.x, self.y - self.size * 1.5),
|
|
75
|
+
(self.x + self.size * 0.5, self.y - self.size * 0.5),
|
|
76
|
+
(self.x + self.size * 1.5, self.y),
|
|
77
|
+
(self.x + self.size * 0.5, self.y + self.size * 0.5),
|
|
78
|
+
(self.x, self.y + self.size * 1.5),
|
|
79
|
+
(self.x - self.size * 0.5, self.y + self.size * 0.5),
|
|
80
|
+
(self.x - self.size * 1.5, self.y),
|
|
81
|
+
(self.x - self.size * 0.5, self.y - self.size * 0.5),
|
|
82
|
+
]
|
|
83
|
+
draw.polygon(points, fill=color)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List, Tuple, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class ColorSchema:
|
|
8
|
+
background: Union[str, Tuple[int, int, int]]
|
|
9
|
+
text_colors: List[Union[str, Tuple[int, int, int]]]
|
|
10
|
+
background_colors: List[Union[str, Tuple[int, int, int]]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Theme(Enum):
|
|
14
|
+
LIGHT = "light"
|
|
15
|
+
DARK = "dark"
|
|
16
|
+
CUSTOM = "custom"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Difficulty(Enum):
|
|
20
|
+
EASY = "easy"
|
|
21
|
+
MEDIUM = "medium"
|
|
22
|
+
HARD = "hard"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
LIGHT_SCHEMA = ColorSchema(
|
|
26
|
+
background=(255, 255, 255),
|
|
27
|
+
text_colors=[
|
|
28
|
+
(20, 20, 20), # Dark Gray
|
|
29
|
+
(0, 51, 102), # Deep Blue
|
|
30
|
+
(102, 0, 0), # Dark Red
|
|
31
|
+
(0, 102, 51), # Dark Green
|
|
32
|
+
],
|
|
33
|
+
background_colors=[
|
|
34
|
+
(240, 240, 240, 60),
|
|
35
|
+
(255, 228, 225, 60),
|
|
36
|
+
(224, 255, 255, 60),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
DARK_SCHEMA = ColorSchema(
|
|
41
|
+
background=(10, 10, 10),
|
|
42
|
+
text_colors=[
|
|
43
|
+
(0, 255, 255), # Cyan
|
|
44
|
+
(255, 0, 255), # Magenta
|
|
45
|
+
(255, 255, 0), # Yellow
|
|
46
|
+
(0, 255, 0), # Neon Green
|
|
47
|
+
],
|
|
48
|
+
background_colors=[
|
|
49
|
+
(40, 40, 40, 80),
|
|
50
|
+
(60, 60, 60, 80),
|
|
51
|
+
(30, 30, 50, 80),
|
|
52
|
+
]
|
|
53
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotcha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gestalt Illusion Captcha Generator for Python
|
|
5
|
+
Author: Dotcha Authors
|
|
6
|
+
License-Expression: Unlicense
|
|
7
|
+
Project-URL: Homepage, https://github.com/trombalny/dotcha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: Pillow>=9.0.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Dotcha 🌀
|
|
19
|
+
|
|
20
|
+
**Dotcha** is a high-performance Python library for generating captchas based on the **Gestalt Illusion** principle. It creates visual patterns from thousands of geometric shapes that are intuitive for humans but highly resistant to automated OCR and AI solvers.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
- 🎨 **Gestalt Illusion**: Pure geometric rendering for maximum machine-learning resistance.
|
|
24
|
+
- ⚡ **High Performance**: Optimized rendering (~30ms per PNG).
|
|
25
|
+
- 🔄 **Animated GIFs**: "Temporal Gestalt" effect where text is reconstruction through motion.
|
|
26
|
+
- 🛡️ **Fuzzy Validation**: Intelligent answer verification with Levenshtein distance.
|
|
27
|
+
- 🌓 **Universal Themes**: Light, Dark, and fully customizable color schemas.
|
|
28
|
+
- 📉 **Scalable Difficulty**: Dynamic calibration from clear patterns to chaotic noise.
|
|
29
|
+
- 🤖 **Async Ready**: Native `asyncio` support for web frameworks and bots.
|
|
30
|
+
|
|
31
|
+
## Visual Examples
|
|
32
|
+
|
|
33
|
+
| Light Theme (EASY) | Dark Theme (MEDIUM) | Animated (Temporal Gestalt) |
|
|
34
|
+
|:---:|:---:|:---:|
|
|
35
|
+
|  |  |  |
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install Pillow
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Synchronous PNG
|
|
46
|
+
```python
|
|
47
|
+
from dotcha import CaptchaGenerator, Theme
|
|
48
|
+
|
|
49
|
+
gen = CaptchaGenerator(theme=Theme.LIGHT)
|
|
50
|
+
text, buffer = gen.generate()
|
|
51
|
+
|
|
52
|
+
with open("captcha.png", "wb") as f:
|
|
53
|
+
f.write(buffer.read())
|
|
54
|
+
print(f"Generated captcha: {text}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Asynchronous GIF
|
|
58
|
+
```python
|
|
59
|
+
from dotcha import CaptchaGenerator, Theme, Difficulty
|
|
60
|
+
|
|
61
|
+
async def send_captcha():
|
|
62
|
+
gen = CaptchaGenerator(theme=Theme.DARK, difficulty=Difficulty.HARD)
|
|
63
|
+
text, buffer = await gen.agenerate_gif(frames=12)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> 💡 See [examples/bot_demo.py](examples/bot_demo.py) for a complete Telegram bot integration.
|
|
67
|
+
|
|
68
|
+
### Fuzzy Verification
|
|
69
|
+
```python
|
|
70
|
+
from dotcha import CaptchaGenerator
|
|
71
|
+
|
|
72
|
+
user_input = "ABCDE"
|
|
73
|
+
actual = "ABCD1"
|
|
74
|
+
|
|
75
|
+
# Accepts answer with 1 char distance
|
|
76
|
+
is_valid, distance = CaptchaGenerator.check_answer(user_input, actual, fuzzy_tolerance=1)
|
|
77
|
+
if is_valid:
|
|
78
|
+
print(f"Passed! Distance: {distance}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Why Dotcha?
|
|
82
|
+
- **Pattern-Based Security**: While a human brain naturally connects scattered dots into characters, standard OCR algorithms perceive them as disconnected noise.
|
|
83
|
+
- **Temporal Signal**: The GIF format utilizes "Temporal Sparsity". A static frame is unreadable, but the human eye integrates movement into a clear signal.
|
|
84
|
+
- **Zero Disk Footprint**: Captchas are generated directly into byte buffers, making it ideal for high-concurrency environments.
|
|
85
|
+
- **Stable & Lightweight**: Explicit resource management and minimal dependencies.
|
|
86
|
+
|
|
87
|
+
## Performance
|
|
88
|
+
|
|
89
|
+
### Static Images (PNG)
|
|
90
|
+
| Difficulty | Time | Shapes | Description |
|
|
91
|
+
|------------|------|--------|-------------|
|
|
92
|
+
| **EASY** | ~0.05s | 8000 | High density, very clear for humans. |
|
|
93
|
+
| **MEDIUM** | ~0.03s | 5000 | Balanced density and noise. |
|
|
94
|
+
| **HARD** | ~0.02s | 3500 | Sparse text, higher background chaos. |
|
|
95
|
+
|
|
96
|
+
### Animated Captchas (GIF)
|
|
97
|
+
| Difficulty | Time (12 frames) | Security Level |
|
|
98
|
+
|------------|-----------------|----------------|
|
|
99
|
+
| **EASY** | ~0.51s | Maximum (High signal integration) |
|
|
100
|
+
| **MEDIUM** | ~0.38s | Standard |
|
|
101
|
+
| **HARD** | ~0.32s | Extreme (Sparse signal in motion) |
|
|
102
|
+
|
|
103
|
+
> *Note: Timings are based on a standard modern CPU. Generation is offloaded to background threads to keep your bot responsive.*
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
This project is released into the public domain under the [Unlicense](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dotcha/__init__.py
|
|
5
|
+
dotcha/generator.py
|
|
6
|
+
dotcha/shapes.py
|
|
7
|
+
dotcha/theme.py
|
|
8
|
+
dotcha.egg-info/PKG-INFO
|
|
9
|
+
dotcha.egg-info/SOURCES.txt
|
|
10
|
+
dotcha.egg-info/dependency_links.txt
|
|
11
|
+
dotcha.egg-info/requires.txt
|
|
12
|
+
dotcha.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=9.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dotcha
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dotcha"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Dotcha Authors" },
|
|
10
|
+
]
|
|
11
|
+
description = "Gestalt Illusion Captcha Generator for Python"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = "Unlicense"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Multimedia :: Graphics",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"Pillow>=9.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
packages = ["dotcha"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
"Homepage" = "https://github.com/trombalny/dotcha"
|
dotcha-0.1.0/setup.cfg
ADDED