Initialize repository
This commit is contained in:
154
generators/normal_gen.py
Normal file
154
generators/normal_gen.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
普通字符验证码生成器
|
||||
|
||||
生成风格:
|
||||
- 浅色随机背景 (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)
|
||||
Reference in New Issue
Block a user