Initialize repository
This commit is contained in:
211
generators/threed_gen.py
Normal file
211
generators/threed_gen.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
3D 立体验证码生成器
|
||||
|
||||
生成具有 3D 透视/阴影效果的验证码:
|
||||
- 使用仿射变换模拟 3D 透视
|
||||
- 添加阴影效果 (偏移的深色副本)
|
||||
- 字符有深度感和倾斜
|
||||
- 渐变背景增强立体感
|
||||
- 标签: 纯字符内容
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
from config import GENERATE_CONFIG, THREED_CHARS
|
||||
from generators.base import BaseCaptchaGenerator
|
||||
|
||||
# 字体 (粗体效果更好渲染 3D)
|
||||
_FONT_PATHS = [
|
||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSerif-Bold.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSerif-Bold.ttf",
|
||||
"/usr/share/fonts/gnu-free/FreeSansBold.otf",
|
||||
]
|
||||
|
||||
# 前景色 — 鲜艳、对比度高
|
||||
_FRONT_COLORS = [
|
||||
(220, 50, 50), # 红
|
||||
(50, 100, 220), # 蓝
|
||||
(30, 160, 30), # 绿
|
||||
(200, 150, 0), # 金
|
||||
(180, 50, 180), # 紫
|
||||
(0, 160, 160), # 青
|
||||
(220, 100, 0), # 橙
|
||||
]
|
||||
|
||||
|
||||
class ThreeDCaptchaGenerator(BaseCaptchaGenerator):
|
||||
"""3D 立体验证码生成器。"""
|
||||
|
||||
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["3d"]
|
||||
self.chars = THREED_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. 渐变背景 (增强立体感)
|
||||
img = self._gradient_background(rng)
|
||||
|
||||
# 3. 逐字符绘制 (阴影 + 透视 + 前景)
|
||||
self._draw_3d_text(img, text, rng)
|
||||
|
||||
# 4. 干扰线 (较粗、有深度感)
|
||||
self._draw_depth_lines(img, rng)
|
||||
|
||||
# 5. 轻微高斯模糊
|
||||
img = img.filter(ImageFilter.GaussianBlur(radius=0.7))
|
||||
|
||||
return img, text
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 私有方法
|
||||
# ----------------------------------------------------------
|
||||
def _gradient_background(self, rng: random.Random) -> Image.Image:
|
||||
"""生成从上到下的浅色渐变背景。"""
|
||||
img = Image.new("RGB", (self.width, self.height))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 随机两个浅色
|
||||
c1 = tuple(rng.randint(200, 240) for _ in range(3))
|
||||
c2 = tuple(rng.randint(180, 220) for _ in range(3))
|
||||
|
||||
for y in range(self.height):
|
||||
ratio = y / max(self.height - 1, 1)
|
||||
r = int(c1[0] + (c2[0] - c1[0]) * ratio)
|
||||
g = int(c1[1] + (c2[1] - c1[1]) * ratio)
|
||||
b = int(c1[2] + (c2[2] - c1[2]) * ratio)
|
||||
draw.line([(0, y), (self.width, y)], fill=(r, g, b))
|
||||
|
||||
return img
|
||||
|
||||
def _draw_3d_text(self, img: Image.Image, text: str, rng: random.Random) -> None:
|
||||
"""逐字符绘制 3D 效果: 阴影层 + 透视变换 + 前景层。"""
|
||||
n = len(text)
|
||||
slot_w = self.width // n
|
||||
font_size = int(min(slot_w * 0.8, self.height * 0.65))
|
||||
font_size = max(font_size, 16)
|
||||
|
||||
shadow_dx, shadow_dy = self.cfg["shadow_offset"]
|
||||
|
||||
for i, ch in enumerate(text):
|
||||
font_path = rng.choice(self._fonts)
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
front_color = rng.choice(_FRONT_COLORS)
|
||||
# 阴影色: 对应前景色的暗化版本
|
||||
shadow_color = tuple(max(0, c - 80) for c in front_color)
|
||||
|
||||
# 渲染单字符
|
||||
bbox = font.getbbox(ch)
|
||||
cw = bbox[2] - bbox[0] + 8
|
||||
ch_h = bbox[3] - bbox[1] + 8
|
||||
pad = max(shadow_dx, shadow_dy) + 4 # 额外空间给阴影
|
||||
|
||||
canvas_w = cw + pad * 2
|
||||
canvas_h = ch_h + pad * 2
|
||||
|
||||
# --- 阴影层 ---
|
||||
shadow_img = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(shadow_img).text(
|
||||
(-bbox[0] + pad + shadow_dx, -bbox[1] + pad + shadow_dy),
|
||||
ch, fill=shadow_color + (180,), font=font
|
||||
)
|
||||
|
||||
# --- 前景层 ---
|
||||
front_img = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(front_img).text(
|
||||
(-bbox[0] + pad, -bbox[1] + pad),
|
||||
ch, fill=front_color + (255,), font=font
|
||||
)
|
||||
|
||||
# 合并: 先阴影后前景
|
||||
char_img = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
|
||||
char_img = Image.alpha_composite(char_img, shadow_img)
|
||||
char_img = Image.alpha_composite(char_img, front_img)
|
||||
|
||||
# 透视变换 (仿射)
|
||||
char_img = self._perspective_transform(char_img, rng)
|
||||
|
||||
# 随机旋转
|
||||
angle = rng.randint(-20, 20)
|
||||
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(-4, 4)
|
||||
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 _perspective_transform(self, img: Image.Image, rng: random.Random) -> Image.Image:
|
||||
"""对单个字符图片施加仿射变换模拟 3D 透视。"""
|
||||
w, h = img.size
|
||||
intensity = self.cfg["perspective_intensity"]
|
||||
|
||||
# 随机 shear / scale 参数
|
||||
shear_x = rng.uniform(-intensity, intensity)
|
||||
shear_y = rng.uniform(-intensity * 0.5, intensity * 0.5)
|
||||
scale_x = rng.uniform(1.0 - intensity * 0.3, 1.0 + intensity * 0.3)
|
||||
scale_y = rng.uniform(1.0 - intensity * 0.3, 1.0 + intensity * 0.3)
|
||||
|
||||
# 仿射变换矩阵 (a, b, c, d, e, f) -> (x', y') = (a*x+b*y+c, d*x+e*y+f)
|
||||
# Pillow transform 需要逆变换系数
|
||||
a = scale_x
|
||||
b = shear_x
|
||||
d = shear_y
|
||||
e = scale_y
|
||||
# 计算偏移让中心不变
|
||||
c = (1 - a) * w / 2 - b * h / 2
|
||||
f = -d * w / 2 + (1 - e) * h / 2
|
||||
|
||||
return img.transform(
|
||||
(w, h), Image.AFFINE,
|
||||
(a, b, c, d, e, f),
|
||||
resample=Image.BICUBIC
|
||||
)
|
||||
|
||||
def _draw_depth_lines(self, img: Image.Image, rng: random.Random) -> None:
|
||||
"""绘制有深度感的干扰线 (较粗、带阴影)。"""
|
||||
draw = ImageDraw.Draw(img)
|
||||
num = rng.randint(2, 4)
|
||||
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)
|
||||
|
||||
# 阴影线
|
||||
shadow_color = tuple(rng.randint(80, 130) for _ in range(3))
|
||||
dx, dy = self.cfg["shadow_offset"]
|
||||
draw.line([(x1 + dx, y1 + dy), (x2 + dx, y2 + dy)],
|
||||
fill=shadow_color, width=rng.randint(2, 3))
|
||||
|
||||
# 前景线
|
||||
color = tuple(rng.randint(120, 200) for _ in range(3))
|
||||
draw.line([(x1, y1), (x2, y2)], fill=color, width=rng.randint(1, 2))
|
||||
Reference in New Issue
Block a user