easy-captcha-python 0.1.0__py3-none-any.whl
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.
- easy_captcha/__init__.py +40 -0
- easy_captcha/base/__init__.py +17 -0
- easy_captcha/base/arithmetic_captcha_abstract.py +79 -0
- easy_captcha/base/captcha.py +337 -0
- easy_captcha/base/chinese_captcha_abstract.py +81 -0
- easy_captcha/base/randoms.py +77 -0
- easy_captcha/captcha/__init__.py +19 -0
- easy_captcha/captcha/arithmetic_captcha.py +109 -0
- easy_captcha/captcha/chinese_captcha.py +111 -0
- easy_captcha/captcha/chinese_gif_captcha.py +180 -0
- easy_captcha/captcha/gif_captcha.py +180 -0
- easy_captcha/captcha/spec_captcha.py +112 -0
- easy_captcha/constants.py +55 -0
- easy_captcha/fonts/actionj.ttf +0 -0
- easy_captcha/fonts/epilog.ttf +0 -0
- easy_captcha/fonts/fresnel.ttf +0 -0
- easy_captcha/fonts/headache.ttf +0 -0
- easy_captcha/fonts/lexo.ttf +0 -0
- easy_captcha/fonts/prefix.ttf +0 -0
- easy_captcha/fonts/progbot.ttf +0 -0
- easy_captcha/fonts/ransom.ttf +0 -0
- easy_captcha/fonts/robot.ttf +0 -0
- easy_captcha/fonts/scandal.ttf +0 -0
- easy_captcha_python-0.1.0.dist-info/METADATA +445 -0
- easy_captcha_python-0.1.0.dist-info/RECORD +28 -0
- easy_captcha_python-0.1.0.dist-info/WHEEL +5 -0
- easy_captcha_python-0.1.0.dist-info/licenses/LICENSE +674 -0
- easy_captcha_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
算术验证码(PNG格式)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from PIL import Image, ImageDraw
|
|
8
|
+
|
|
9
|
+
from ..base.arithmetic_captcha_abstract import ArithmeticCaptchaAbstract
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArithmeticCaptcha(ArithmeticCaptchaAbstract):
|
|
13
|
+
"""算术验证码(PNG格式)"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width: int = 130, height: int = 48, length: int = 2):
|
|
16
|
+
"""
|
|
17
|
+
初始化算术验证码
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
width: 验证码宽度
|
|
21
|
+
height: 验证码高度
|
|
22
|
+
length: 运算数字的位数(默认2位数运算)
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._width = width
|
|
26
|
+
self._height = height
|
|
27
|
+
self._len = length
|
|
28
|
+
|
|
29
|
+
def out(self, stream: BytesIO) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
输出验证码到流
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stream: 输出流
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: 是否成功
|
|
38
|
+
"""
|
|
39
|
+
self.check_alpha()
|
|
40
|
+
# 显示算术公式而不是结果
|
|
41
|
+
return self._graphics_image(list(self.get_arithmetic_string()), stream)
|
|
42
|
+
|
|
43
|
+
def to_base64(self, prefix: str = "data:image/png;base64,") -> str:
|
|
44
|
+
"""
|
|
45
|
+
输出base64编码
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prefix: base64前缀
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: base64编码字符串
|
|
52
|
+
"""
|
|
53
|
+
return self._to_base64_impl(prefix)
|
|
54
|
+
|
|
55
|
+
def _graphics_image(self, chars, stream: BytesIO) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
生成验证码图像
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
chars: 验证码字符列表(算术公式)
|
|
61
|
+
stream: 输出流
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
bool: 是否成功
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
# 创建图像
|
|
68
|
+
image = Image.new('RGB', (self._width, self._height), 'white')
|
|
69
|
+
draw = ImageDraw.Draw(image)
|
|
70
|
+
|
|
71
|
+
# 绘制干扰圆
|
|
72
|
+
self.draw_oval(2, draw)
|
|
73
|
+
|
|
74
|
+
# 绘制验证码文字
|
|
75
|
+
font = self.get_font()
|
|
76
|
+
|
|
77
|
+
# 计算每个字符的宽度
|
|
78
|
+
char_width = self._width // len(chars)
|
|
79
|
+
|
|
80
|
+
for i, char in enumerate(chars):
|
|
81
|
+
# 随机颜色
|
|
82
|
+
color = self._color()
|
|
83
|
+
|
|
84
|
+
# 计算字符位置
|
|
85
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
86
|
+
char_w = bbox[2] - bbox[0]
|
|
87
|
+
char_h = bbox[3] - bbox[1]
|
|
88
|
+
|
|
89
|
+
# 字符的左右边距
|
|
90
|
+
char_spacing = (char_width - char_w) // 2
|
|
91
|
+
|
|
92
|
+
# 字符的x坐标
|
|
93
|
+
x = i * char_width + char_spacing + 3
|
|
94
|
+
|
|
95
|
+
# 字符的y坐标(垂直居中)
|
|
96
|
+
y = (self._height - char_h) // 2 - 3
|
|
97
|
+
|
|
98
|
+
# 绘制字符
|
|
99
|
+
draw.text((x, y), char, fill=color, font=font)
|
|
100
|
+
|
|
101
|
+
# 保存为PNG
|
|
102
|
+
image.save(stream, format='PNG')
|
|
103
|
+
stream.seek(0)
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"生成算术验证码失败: {e}")
|
|
108
|
+
return False
|
|
109
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
中文验证码(PNG格式)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from PIL import Image, ImageDraw
|
|
8
|
+
|
|
9
|
+
from ..base.chinese_captcha_abstract import ChineseCaptchaAbstract
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChineseCaptcha(ChineseCaptchaAbstract):
|
|
13
|
+
"""中文验证码(PNG格式)"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width: int = 130, height: int = 48, length: int = 4):
|
|
16
|
+
"""
|
|
17
|
+
初始化中文验证码
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
width: 验证码宽度
|
|
21
|
+
height: 验证码高度
|
|
22
|
+
length: 验证码字符数
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._width = width
|
|
26
|
+
self._height = height
|
|
27
|
+
self._len = length
|
|
28
|
+
|
|
29
|
+
def out(self, stream: BytesIO) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
输出验证码到流
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stream: 输出流
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: 是否成功
|
|
38
|
+
"""
|
|
39
|
+
self.check_alpha()
|
|
40
|
+
return self._graphics_image(self.text_char(), stream)
|
|
41
|
+
|
|
42
|
+
def to_base64(self, prefix: str = "data:image/png;base64,") -> str:
|
|
43
|
+
"""
|
|
44
|
+
输出base64编码
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
prefix: base64前缀
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: base64编码字符串
|
|
51
|
+
"""
|
|
52
|
+
return self._to_base64_impl(prefix)
|
|
53
|
+
|
|
54
|
+
def _graphics_image(self, chars, stream: BytesIO) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
生成验证码图像
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
chars: 验证码字符列表
|
|
60
|
+
stream: 输出流
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
bool: 是否成功
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
# 创建图像
|
|
67
|
+
image = Image.new('RGB', (self._width, self._height), 'white')
|
|
68
|
+
draw = ImageDraw.Draw(image)
|
|
69
|
+
|
|
70
|
+
# 绘制干扰圆
|
|
71
|
+
self.draw_oval(3, draw)
|
|
72
|
+
|
|
73
|
+
# 绘制干扰线(贝塞尔曲线)
|
|
74
|
+
self.draw_bezier_curve(1, draw)
|
|
75
|
+
|
|
76
|
+
# 绘制验证码文字
|
|
77
|
+
font = self.get_font()
|
|
78
|
+
|
|
79
|
+
# 计算每个字符的宽度
|
|
80
|
+
char_width = self._width // len(chars)
|
|
81
|
+
|
|
82
|
+
for i, char in enumerate(chars):
|
|
83
|
+
# 随机颜色
|
|
84
|
+
color = self._color()
|
|
85
|
+
|
|
86
|
+
# 计算字符位置
|
|
87
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
88
|
+
char_w = bbox[2] - bbox[0]
|
|
89
|
+
char_h = bbox[3] - bbox[1]
|
|
90
|
+
|
|
91
|
+
# 字符的左右边距
|
|
92
|
+
char_spacing = (char_width - char_w) // 2
|
|
93
|
+
|
|
94
|
+
# 字符的x坐标
|
|
95
|
+
x = i * char_width + char_spacing + 3
|
|
96
|
+
|
|
97
|
+
# 字符的y坐标(垂直居中)
|
|
98
|
+
y = (self._height - char_h) // 2 - 3
|
|
99
|
+
|
|
100
|
+
# 绘制字符
|
|
101
|
+
draw.text((x, y), char, fill=color, font=font)
|
|
102
|
+
|
|
103
|
+
# 保存为PNG
|
|
104
|
+
image.save(stream, format='PNG')
|
|
105
|
+
stream.seek(0)
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"生成中文验证码失败: {e}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
中文GIF动画验证码
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from PIL import Image, ImageDraw
|
|
8
|
+
|
|
9
|
+
from ..base.chinese_captcha_abstract import ChineseCaptchaAbstract
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChineseGifCaptcha(ChineseCaptchaAbstract):
|
|
13
|
+
"""中文GIF动画验证码"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width: int = 130, height: int = 48, length: int = 4):
|
|
16
|
+
"""
|
|
17
|
+
初始化中文GIF验证码
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
width: 验证码宽度
|
|
21
|
+
height: 验证码高度
|
|
22
|
+
length: 验证码字符数
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._width = width
|
|
26
|
+
self._height = height
|
|
27
|
+
self._len = length
|
|
28
|
+
|
|
29
|
+
def out(self, stream: BytesIO) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
输出验证码到流
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stream: 输出流
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: 是否成功
|
|
38
|
+
"""
|
|
39
|
+
self.check_alpha()
|
|
40
|
+
chars = self.text_char()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# 为每个字符生成随机颜色
|
|
44
|
+
font_colors = [self._color() for _ in range(self._len)]
|
|
45
|
+
|
|
46
|
+
# 生成贝塞尔曲线参数
|
|
47
|
+
x1 = 5
|
|
48
|
+
y1 = self.num(5, self._height // 2)
|
|
49
|
+
x2 = self._width - 5
|
|
50
|
+
y2 = self.num(self._height // 2, self._height - 5)
|
|
51
|
+
ctrlx = self.num(self._width // 4, self._width * 3 // 4)
|
|
52
|
+
ctrly = self.num(5, self._height - 5)
|
|
53
|
+
|
|
54
|
+
if self.num(2) == 0:
|
|
55
|
+
y1, y2 = y2, y1
|
|
56
|
+
|
|
57
|
+
ctrlx1 = self.num(self._width // 4, self._width * 3 // 4)
|
|
58
|
+
ctrly1 = self.num(5, self._height - 5)
|
|
59
|
+
|
|
60
|
+
bezier_params = [(x1, y1), (ctrlx, ctrly), (ctrlx1, ctrly1), (x2, y2)]
|
|
61
|
+
|
|
62
|
+
# 生成每一帧
|
|
63
|
+
frames = []
|
|
64
|
+
for i in range(self._len):
|
|
65
|
+
frame = self._graphics_image(font_colors, chars, i, bezier_params)
|
|
66
|
+
frames.append(frame)
|
|
67
|
+
|
|
68
|
+
# 保存为GIF
|
|
69
|
+
frames[0].save(
|
|
70
|
+
stream,
|
|
71
|
+
format='GIF',
|
|
72
|
+
save_all=True,
|
|
73
|
+
append_images=frames[1:],
|
|
74
|
+
duration=100, # 每帧100ms
|
|
75
|
+
loop=0 # 无限循环
|
|
76
|
+
)
|
|
77
|
+
stream.seek(0)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"生成中文GIF验证码失败: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def to_base64(self, prefix: str = "data:image/gif;base64,") -> str:
|
|
85
|
+
"""
|
|
86
|
+
输出base64编码
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
prefix: base64前缀
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: base64编码字符串
|
|
93
|
+
"""
|
|
94
|
+
return self._to_base64_impl(prefix)
|
|
95
|
+
|
|
96
|
+
def _graphics_image(self, font_colors, chars, flag, bezier_params):
|
|
97
|
+
"""
|
|
98
|
+
生成单帧图像
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
font_colors: 字体颜色列表
|
|
102
|
+
chars: 字符列表
|
|
103
|
+
flag: 当前帧索引
|
|
104
|
+
bezier_params: 贝塞尔曲线参数
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Image: PIL图像对象
|
|
108
|
+
"""
|
|
109
|
+
# 创建图像
|
|
110
|
+
image = Image.new('RGB', (self._width, self._height), 'white')
|
|
111
|
+
draw = ImageDraw.Draw(image)
|
|
112
|
+
|
|
113
|
+
# 绘制干扰圆
|
|
114
|
+
self.draw_oval(2, draw)
|
|
115
|
+
|
|
116
|
+
# 绘制贝塞尔曲线
|
|
117
|
+
curve_points = self._cubic_bezier_points(
|
|
118
|
+
bezier_params[0], bezier_params[1],
|
|
119
|
+
bezier_params[2], bezier_params[3], 50
|
|
120
|
+
)
|
|
121
|
+
draw.line(curve_points, fill=font_colors[0], width=2)
|
|
122
|
+
|
|
123
|
+
# 绘制验证码文字
|
|
124
|
+
font = self.get_font()
|
|
125
|
+
char_width = self._width // len(chars)
|
|
126
|
+
|
|
127
|
+
for i, char in enumerate(chars):
|
|
128
|
+
# 计算透明度(渐变效果)
|
|
129
|
+
alpha = self._get_alpha(flag, i)
|
|
130
|
+
|
|
131
|
+
# 根据透明度调整颜色亮度
|
|
132
|
+
color = self._adjust_color_alpha(font_colors[i], alpha)
|
|
133
|
+
|
|
134
|
+
# 计算字符位置
|
|
135
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
136
|
+
char_w = bbox[2] - bbox[0]
|
|
137
|
+
char_h = bbox[3] - bbox[1]
|
|
138
|
+
|
|
139
|
+
char_spacing = (char_width - char_w) // 2
|
|
140
|
+
x = i * char_width + char_spacing - 3
|
|
141
|
+
y = (self._height - char_h) // 2 - 3
|
|
142
|
+
|
|
143
|
+
# 绘制字符
|
|
144
|
+
draw.text((x, y), char, fill=color, font=font)
|
|
145
|
+
|
|
146
|
+
return image
|
|
147
|
+
|
|
148
|
+
def _get_alpha(self, i, j):
|
|
149
|
+
"""
|
|
150
|
+
获取透明度,从0到1,自动计算步长
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
i: 当前帧索引
|
|
154
|
+
j: 字符索引
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
float: 透明度值 (0-1)
|
|
158
|
+
"""
|
|
159
|
+
num = i + j
|
|
160
|
+
r = 1.0 / (self._len - 1) if self._len > 1 else 1.0
|
|
161
|
+
s = self._len * r
|
|
162
|
+
return (num * r - s) if num >= self._len else num * r
|
|
163
|
+
|
|
164
|
+
def _adjust_color_alpha(self, color, alpha):
|
|
165
|
+
"""
|
|
166
|
+
根据透明度调整颜色
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
color: RGB颜色元组
|
|
170
|
+
alpha: 透明度 (0-1)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
tuple: 调整后的RGB颜色
|
|
174
|
+
"""
|
|
175
|
+
# 简单的透明度模拟:与白色混合
|
|
176
|
+
r = int(color[0] * alpha + 255 * (1 - alpha))
|
|
177
|
+
g = int(color[1] * alpha + 255 * (1 - alpha))
|
|
178
|
+
b = int(color[2] * alpha + 255 * (1 - alpha))
|
|
179
|
+
return (r, g, b)
|
|
180
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
GIF动画验证码
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from PIL import Image, ImageDraw
|
|
8
|
+
|
|
9
|
+
from ..base.captcha import Captcha
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GifCaptcha(Captcha):
|
|
13
|
+
"""GIF动画验证码"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width: int = 130, height: int = 48, length: int = 5):
|
|
16
|
+
"""
|
|
17
|
+
初始化GIF验证码
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
width: 验证码宽度
|
|
21
|
+
height: 验证码高度
|
|
22
|
+
length: 验证码字符数
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._width = width
|
|
26
|
+
self._height = height
|
|
27
|
+
self._len = length
|
|
28
|
+
|
|
29
|
+
def out(self, stream: BytesIO) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
输出验证码到流
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stream: 输出流
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: 是否成功
|
|
38
|
+
"""
|
|
39
|
+
self.check_alpha()
|
|
40
|
+
chars = self.text_char()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# 为每个字符生成随机颜色
|
|
44
|
+
font_colors = [self._color() for _ in range(self._len)]
|
|
45
|
+
|
|
46
|
+
# 生成贝塞尔曲线参数
|
|
47
|
+
x1 = 5
|
|
48
|
+
y1 = self.num(5, self._height // 2)
|
|
49
|
+
x2 = self._width - 5
|
|
50
|
+
y2 = self.num(self._height // 2, self._height - 5)
|
|
51
|
+
ctrlx = self.num(self._width // 4, self._width * 3 // 4)
|
|
52
|
+
ctrly = self.num(5, self._height - 5)
|
|
53
|
+
|
|
54
|
+
if self.num(2) == 0:
|
|
55
|
+
y1, y2 = y2, y1
|
|
56
|
+
|
|
57
|
+
ctrlx1 = self.num(self._width // 4, self._width * 3 // 4)
|
|
58
|
+
ctrly1 = self.num(5, self._height - 5)
|
|
59
|
+
|
|
60
|
+
bezier_params = [(x1, y1), (ctrlx, ctrly), (ctrlx1, ctrly1), (x2, y2)]
|
|
61
|
+
|
|
62
|
+
# 生成每一帧
|
|
63
|
+
frames = []
|
|
64
|
+
for i in range(self._len):
|
|
65
|
+
frame = self._graphics_image(font_colors, chars, i, bezier_params)
|
|
66
|
+
frames.append(frame)
|
|
67
|
+
|
|
68
|
+
# 保存为GIF
|
|
69
|
+
frames[0].save(
|
|
70
|
+
stream,
|
|
71
|
+
format='GIF',
|
|
72
|
+
save_all=True,
|
|
73
|
+
append_images=frames[1:],
|
|
74
|
+
duration=100, # 每帧100ms
|
|
75
|
+
loop=0 # 无限循环
|
|
76
|
+
)
|
|
77
|
+
stream.seek(0)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"生成GIF验证码失败: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def to_base64(self, prefix: str = "data:image/gif;base64,") -> str:
|
|
85
|
+
"""
|
|
86
|
+
输出base64编码
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
prefix: base64前缀
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: base64编码字符串
|
|
93
|
+
"""
|
|
94
|
+
return self._to_base64_impl(prefix)
|
|
95
|
+
|
|
96
|
+
def _graphics_image(self, font_colors, chars, flag, bezier_params):
|
|
97
|
+
"""
|
|
98
|
+
生成单帧图像
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
font_colors: 字体颜色列表
|
|
102
|
+
chars: 字符列表
|
|
103
|
+
flag: 当前帧索引
|
|
104
|
+
bezier_params: 贝塞尔曲线参数
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Image: PIL图像对象
|
|
108
|
+
"""
|
|
109
|
+
# 创建图像
|
|
110
|
+
image = Image.new('RGB', (self._width, self._height), 'white')
|
|
111
|
+
draw = ImageDraw.Draw(image)
|
|
112
|
+
|
|
113
|
+
# 绘制干扰圆(带透明度效果)
|
|
114
|
+
self.draw_oval(2, draw)
|
|
115
|
+
|
|
116
|
+
# 绘制贝塞尔曲线
|
|
117
|
+
curve_points = self._cubic_bezier_points(
|
|
118
|
+
bezier_params[0], bezier_params[1],
|
|
119
|
+
bezier_params[2], bezier_params[3], 50
|
|
120
|
+
)
|
|
121
|
+
draw.line(curve_points, fill=font_colors[0], width=2)
|
|
122
|
+
|
|
123
|
+
# 绘制验证码文字
|
|
124
|
+
font = self.get_font()
|
|
125
|
+
char_width = self._width // len(chars)
|
|
126
|
+
|
|
127
|
+
for i, char in enumerate(chars):
|
|
128
|
+
# 计算透明度(渐变效果)
|
|
129
|
+
alpha = self._get_alpha(flag, i)
|
|
130
|
+
|
|
131
|
+
# 根据透明度调整颜色亮度
|
|
132
|
+
color = self._adjust_color_alpha(font_colors[i], alpha)
|
|
133
|
+
|
|
134
|
+
# 计算字符位置
|
|
135
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
136
|
+
char_w = bbox[2] - bbox[0]
|
|
137
|
+
char_h = bbox[3] - bbox[1]
|
|
138
|
+
|
|
139
|
+
char_spacing = (char_width - char_w) // 2
|
|
140
|
+
x = i * char_width + char_spacing - 3
|
|
141
|
+
y = (self._height - char_h) // 2 - 3
|
|
142
|
+
|
|
143
|
+
# 绘制字符
|
|
144
|
+
draw.text((x, y), char, fill=color, font=font)
|
|
145
|
+
|
|
146
|
+
return image
|
|
147
|
+
|
|
148
|
+
def _get_alpha(self, i, j):
|
|
149
|
+
"""
|
|
150
|
+
获取透明度,从0到1,自动计算步长
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
i: 当前帧索引
|
|
154
|
+
j: 字符索引
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
float: 透明度值 (0-1)
|
|
158
|
+
"""
|
|
159
|
+
num = i + j
|
|
160
|
+
r = 1.0 / (self._len - 1) if self._len > 1 else 1.0
|
|
161
|
+
s = self._len * r
|
|
162
|
+
return (num * r - s) if num >= self._len else num * r
|
|
163
|
+
|
|
164
|
+
def _adjust_color_alpha(self, color, alpha):
|
|
165
|
+
"""
|
|
166
|
+
根据透明度调整颜色
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
color: RGB颜色元组
|
|
170
|
+
alpha: 透明度 (0-1)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
tuple: 调整后的RGB颜色
|
|
174
|
+
"""
|
|
175
|
+
# 简单的透明度模拟:与白色混合
|
|
176
|
+
r = int(color[0] * alpha + 255 * (1 - alpha))
|
|
177
|
+
g = int(color[1] * alpha + 255 * (1 - alpha))
|
|
178
|
+
b = int(color[2] * alpha + 255 * (1 - alpha))
|
|
179
|
+
return (r, g, b)
|
|
180
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
PNG格式验证码
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from PIL import Image, ImageDraw
|
|
8
|
+
|
|
9
|
+
from ..base.captcha import Captcha
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpecCaptcha(Captcha):
|
|
13
|
+
"""PNG格式验证码"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width: int = 130, height: int = 48, length: int = 5):
|
|
16
|
+
"""
|
|
17
|
+
初始化PNG验证码
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
width: 验证码宽度
|
|
21
|
+
height: 验证码高度
|
|
22
|
+
length: 验证码字符数
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._width = width
|
|
26
|
+
self._height = height
|
|
27
|
+
self._len = length
|
|
28
|
+
|
|
29
|
+
def out(self, stream: BytesIO) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
输出验证码到流
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stream: 输出流
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: 是否成功
|
|
38
|
+
"""
|
|
39
|
+
self.check_alpha()
|
|
40
|
+
return self._graphics_image(self.text_char(), stream)
|
|
41
|
+
|
|
42
|
+
def to_base64(self, prefix: str = "data:image/png;base64,") -> str:
|
|
43
|
+
"""
|
|
44
|
+
输出base64编码
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
prefix: base64前缀
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: base64编码字符串
|
|
51
|
+
"""
|
|
52
|
+
return self._to_base64_impl(prefix)
|
|
53
|
+
|
|
54
|
+
def _graphics_image(self, chars, stream: BytesIO) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
生成验证码图像
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
chars: 验证码字符列表
|
|
60
|
+
stream: 输出流
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
bool: 是否成功
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
# 创建图像
|
|
67
|
+
image = Image.new('RGB', (self._width, self._height), 'white')
|
|
68
|
+
draw = ImageDraw.Draw(image)
|
|
69
|
+
|
|
70
|
+
# 绘制干扰圆
|
|
71
|
+
self.draw_oval(2, draw)
|
|
72
|
+
|
|
73
|
+
# 绘制干扰线(贝塞尔曲线)
|
|
74
|
+
self.draw_bezier_curve(1, draw)
|
|
75
|
+
|
|
76
|
+
# 绘制验证码文字
|
|
77
|
+
font = self.get_font()
|
|
78
|
+
|
|
79
|
+
# 计算每个字符的宽度
|
|
80
|
+
char_width = self._width // len(chars)
|
|
81
|
+
|
|
82
|
+
for i, char in enumerate(chars):
|
|
83
|
+
# 随机颜色
|
|
84
|
+
color = self._color()
|
|
85
|
+
|
|
86
|
+
# 计算字符位置
|
|
87
|
+
# 获取字符边界框
|
|
88
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
89
|
+
char_w = bbox[2] - bbox[0]
|
|
90
|
+
char_h = bbox[3] - bbox[1]
|
|
91
|
+
|
|
92
|
+
# 字符的左右边距
|
|
93
|
+
char_spacing = (char_width - char_w) // 2
|
|
94
|
+
|
|
95
|
+
# 字符的x坐标
|
|
96
|
+
x = i * char_width + char_spacing + 3
|
|
97
|
+
|
|
98
|
+
# 字符的y坐标(垂直居中)
|
|
99
|
+
y = (self._height - char_h) // 2 - 3
|
|
100
|
+
|
|
101
|
+
# 绘制字符
|
|
102
|
+
draw.text((x, y), char, fill=color, font=font)
|
|
103
|
+
|
|
104
|
+
# 保存为PNG
|
|
105
|
+
image.save(stream, format='PNG')
|
|
106
|
+
stream.seek(0)
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"生成验证码失败: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|