""" 3D 旋转验证码生成器 生成旋转验证码:圆盘上绘制字符 + 方向标记,随机旋转 0-359°。 用户需将圆盘旋转到正确角度。 标签 = 旋转角度(整数) 文件名格式: {angle}_{index:06d}.png """ import random from PIL import Image, ImageDraw, ImageFilter, ImageFont from config import GENERATE_CONFIG, THREED_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/liberation/LiberationSans-Bold.ttf", "/usr/share/fonts/liberation/LiberationSerif-Bold.ttf", "/usr/share/fonts/gnu-free/FreeSansBold.otf", ] _DISC_COLORS = [ (180, 200, 220), (200, 220, 200), (220, 200, 190), (200, 200, 220), (210, 210, 200), ] class ThreeDRotateGenerator(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_rotate"] 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 # 随机旋转角度 0-359 angle = rng.randint(0, 359) if text is None: text = str(angle) # 1. 背景 bg_val = rng.randint(*self.cfg["bg_color_range"]) img = Image.new("RGB", (self.width, self.height), (bg_val, bg_val, bg_val)) draw = ImageDraw.Draw(img) # 2. 绘制圆盘 cx, cy = self.width // 2, self.height // 2 r = self.cfg["disc_radius"] disc_color = rng.choice(_DISC_COLORS) draw.ellipse( [cx - r, cy - r, cx + r, cy + r], fill=disc_color, outline=(100, 100, 100), width=2, ) # 3. 在圆盘上绘制字符和方向标记 (未旋转状态) disc_img = Image.new("RGBA", (r * 2 + 4, r * 2 + 4), (0, 0, 0, 0)) disc_draw = ImageDraw.Draw(disc_img) dc = r + 2 # disc center # 字符 (圆盘中心) font_path = rng.choice(self._fonts) font_size = int(r * 0.6) font = ImageFont.truetype(font_path, font_size) ch = rng.choice(self.chars) bbox = font.getbbox(ch) tw = bbox[2] - bbox[0] th = bbox[3] - bbox[1] disc_draw.text( (dc - tw // 2 - bbox[0], dc - th // 2 - bbox[1]), ch, fill=(50, 50, 50, 255), font=font, ) # 方向标记 (三角箭头,指向上方) ms = self.cfg["marker_size"] marker_y = dc - r + ms + 2 disc_draw.polygon( [(dc, marker_y - ms), (dc - ms // 2, marker_y), (dc + ms // 2, marker_y)], fill=(220, 60, 60, 255), ) # 4. 旋转圆盘内容 disc_img = disc_img.rotate(-angle, resample=Image.BICUBIC, expand=False) # 5. 粘贴到背景 paste_x = cx - dc paste_y = cy - dc img.paste(disc_img, (paste_x, paste_y), disc_img) # 6. 添加少量噪点 for _ in range(rng.randint(20, 50)): nx, ny = rng.randint(0, self.width - 1), rng.randint(0, self.height - 1) nc = tuple(rng.randint(100, 200) for _ in range(3)) draw.point((nx, ny), fill=nc) # 7. 轻微模糊 img = img.filter(ImageFilter.GaussianBlur(radius=0.5)) return img, text