155 lines
5.5 KiB
Python
155 lines
5.5 KiB
Python
"""
|
|
普通字符验证码生成器
|
|
|
|
生成风格:
|
|
- 浅色随机背景 (RGB 各通道 230-255)
|
|
- 每个字符随机深色 (蓝/红/绿/紫/棕等)
|
|
- 字符数量 4-5 个
|
|
- 字符有 ±15° 随机旋转
|
|
- 2-5 条浅色干扰线
|
|
- 少量噪点
|
|
- 可选轻微高斯模糊
|
|
"""
|
|
|
|
import random
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
from config import GENERATE_CONFIG, NORMAL_CHARS
|
|
from generators.base import BaseCaptchaGenerator
|
|
|
|
# 系统可用字体列表(粗体/常规混合,增加多样性)
|
|
_FONT_PATHS = [
|
|
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
|
"/usr/share/fonts/TTF/DejaVuSerif-Bold.ttf",
|
|
"/usr/share/fonts/TTF/DejaVuSansMono-Bold.ttf",
|
|
"/usr/share/fonts/liberation/LiberationSans-Bold.ttf",
|
|
"/usr/share/fonts/liberation/LiberationMono-Bold.ttf",
|
|
"/usr/share/fonts/liberation/LiberationSerif-Bold.ttf",
|
|
"/usr/share/fonts/gnu-free/FreeSansBold.otf",
|
|
"/usr/share/fonts/gnu-free/FreeMonoBold.otf",
|
|
]
|
|
|
|
# 深色调色板 (R, G, B)
|
|
_DARK_COLORS = [
|
|
(0, 0, 180), # 蓝
|
|
(180, 0, 0), # 红
|
|
(0, 130, 0), # 绿
|
|
(130, 0, 130), # 紫
|
|
(120, 60, 0), # 棕
|
|
(0, 100, 100), # 青
|
|
(80, 80, 0), # 橄榄
|
|
(0, 0, 0), # 黑
|
|
(100, 0, 50), # 暗玫红
|
|
(50, 50, 150), # 钢蓝
|
|
]
|
|
|
|
|
|
class NormalCaptchaGenerator(BaseCaptchaGenerator):
|
|
"""普通字符验证码生成器。"""
|
|
|
|
def __init__(self, seed: int | None = None):
|
|
from config import RANDOM_SEED
|
|
super().__init__(seed=seed if seed is not None else RANDOM_SEED)
|
|
|
|
self.cfg = GENERATE_CONFIG["normal"]
|
|
self.chars = NORMAL_CHARS
|
|
self.width, self.height = self.cfg["image_size"]
|
|
|
|
# 预加载可用字体
|
|
self._fonts: list[str] = []
|
|
for p in _FONT_PATHS:
|
|
try:
|
|
ImageFont.truetype(p, 20)
|
|
self._fonts.append(p)
|
|
except OSError:
|
|
continue
|
|
if not self._fonts:
|
|
raise RuntimeError("未找到任何可用字体,无法生成验证码")
|
|
|
|
# ----------------------------------------------------------
|
|
# 公共接口
|
|
# ----------------------------------------------------------
|
|
def generate(self, text: str | None = None) -> tuple[Image.Image, str]:
|
|
rng = self.rng
|
|
|
|
# 1. 随机文本
|
|
if text is None:
|
|
length = rng.randint(*self.cfg["char_count_range"])
|
|
text = "".join(rng.choices(self.chars, k=length))
|
|
|
|
# 2. 浅色背景
|
|
bg_lo, bg_hi = self.cfg["bg_color_range"]
|
|
bg = tuple(rng.randint(bg_lo, bg_hi) for _ in range(3))
|
|
img = Image.new("RGB", (self.width, self.height), bg)
|
|
|
|
# 3. 逐字符绘制(旋转后粘贴)
|
|
self._draw_text(img, text, rng)
|
|
|
|
# 4. 干扰线
|
|
self._draw_noise_lines(img, rng)
|
|
|
|
# 5. 噪点
|
|
self._draw_noise_points(img, rng)
|
|
|
|
# 6. 轻微高斯模糊
|
|
if self.cfg["blur_radius"] > 0:
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=self.cfg["blur_radius"]))
|
|
|
|
return img, text
|
|
|
|
# ----------------------------------------------------------
|
|
# 私有方法
|
|
# ----------------------------------------------------------
|
|
def _draw_text(self, img: Image.Image, text: str, rng: random.Random) -> None:
|
|
"""逐字符旋转并粘贴到画布上。"""
|
|
n = len(text)
|
|
# 每个字符的水平可用宽度
|
|
slot_w = self.width // n
|
|
font_size = int(min(slot_w * 0.9, self.height * 0.7))
|
|
font_size = max(font_size, 12)
|
|
|
|
for i, ch in enumerate(text):
|
|
font_path = rng.choice(self._fonts)
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
color = rng.choice(_DARK_COLORS)
|
|
|
|
# 绘制单字符到临时透明图层
|
|
bbox = font.getbbox(ch)
|
|
cw = bbox[2] - bbox[0] + 4
|
|
ch_h = bbox[3] - bbox[1] + 4
|
|
char_img = Image.new("RGBA", (cw, ch_h), (0, 0, 0, 0))
|
|
ImageDraw.Draw(char_img).text((-bbox[0] + 2, -bbox[1] + 2), ch, fill=color, font=font)
|
|
|
|
# 随机旋转
|
|
angle = rng.randint(*self.cfg["rotation_range"])
|
|
char_img = char_img.rotate(angle, resample=Image.BICUBIC, expand=True)
|
|
|
|
# 粘贴位置
|
|
x = slot_w * i + (slot_w - char_img.width) // 2
|
|
y = (self.height - char_img.height) // 2 + rng.randint(-3, 3)
|
|
x = max(0, min(x, self.width - char_img.width))
|
|
y = max(0, min(y, self.height - char_img.height))
|
|
|
|
img.paste(char_img, (x, y), char_img)
|
|
|
|
def _draw_noise_lines(self, img: Image.Image, rng: random.Random) -> None:
|
|
"""绘制浅色干扰线。"""
|
|
draw = ImageDraw.Draw(img)
|
|
lo, hi = self.cfg["noise_line_range"]
|
|
num = rng.randint(lo, hi)
|
|
for _ in range(num):
|
|
x1, y1 = rng.randint(0, self.width), rng.randint(0, self.height)
|
|
x2, y2 = rng.randint(0, self.width), rng.randint(0, self.height)
|
|
color = tuple(rng.randint(150, 220) for _ in range(3))
|
|
draw.line([(x1, y1), (x2, y2)], fill=color, width=rng.randint(1, 2))
|
|
|
|
def _draw_noise_points(self, img: Image.Image, rng: random.Random) -> None:
|
|
"""绘制噪点。"""
|
|
draw = ImageDraw.Draw(img)
|
|
for _ in range(self.cfg["noise_point_num"]):
|
|
x = rng.randint(0, self.width - 1)
|
|
y = rng.randint(0, self.height - 1)
|
|
color = tuple(rng.randint(0, 200) for _ in range(3))
|
|
draw.point((x, y), fill=color)
|