""" 3D 滑块验证码生成器 生成滑块拼图验证码:纹理背景 + 拼图缺口 + 拼图块在左侧。 用户需将拼图块滑动到缺口位置。 标签 = 缺口 x 坐标偏移(整数) 文件名格式: {offset}_{index:06d}.png """ import random from PIL import Image, ImageDraw, ImageFilter from config import GENERATE_CONFIG from generators.base import BaseCaptchaGenerator class ThreeDSliderGenerator(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_slider"] self.width, self.height = self.cfg["image_size"] def generate(self, text: str | None = None) -> tuple[Image.Image, str]: rng = self.rng pw, ph = self.cfg["puzzle_size"] gap_x_lo, gap_x_hi = self.cfg["gap_x_range"] # 缺口位置 gap_x = rng.randint(gap_x_lo, gap_x_hi) gap_y = rng.randint(10, self.height - ph - 10) if text is None: text = str(gap_x) # 1. 生成纹理背景 img = self._textured_background(rng) # 2. 从缺口位置截取拼图块内容 piece_content = img.crop((gap_x, gap_y, gap_x + pw, gap_y + ph)).copy() # 3. 绘制缺口 (半透明灰色区域) overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) overlay_draw.rectangle( [gap_x, gap_y, gap_x + pw, gap_y + ph], fill=(80, 80, 80, 160), outline=(60, 60, 60, 200), width=2, ) img = img.convert("RGBA") img = Image.alpha_composite(img, overlay) img = img.convert("RGB") # 4. 绘制拼图块在左侧 piece_x = self.cfg["piece_left_margin"] piece_img = Image.new("RGBA", (pw + 4, ph + 4), (0, 0, 0, 0)) piece_draw = ImageDraw.Draw(piece_img) # 阴影 piece_draw.rectangle([2, 2, pw + 3, ph + 3], fill=(0, 0, 0, 80)) # 内容 piece_img.paste(piece_content, (0, 0)) # 边框 piece_draw.rectangle([0, 0, pw - 1, ph - 1], outline=(255, 255, 255, 200), width=2) img_rgba = img.convert("RGBA") img_rgba.paste(piece_img, (piece_x, gap_y), piece_img) img = img_rgba.convert("RGB") # 5. 轻微模糊 img = img.filter(ImageFilter.GaussianBlur(radius=0.3)) return img, text def _textured_background(self, rng: random.Random) -> Image.Image: """生成带纹理的彩色背景。""" img = Image.new("RGB", (self.width, self.height)) draw = ImageDraw.Draw(img) # 渐变底色 base_r, base_g, base_b = rng.randint(100, 180), rng.randint(100, 180), rng.randint(100, 180) for y in range(self.height): ratio = y / max(self.height - 1, 1) r = int(base_r + 30 * ratio) g = int(base_g - 20 * ratio) b = int(base_b + 10 * ratio) draw.line([(0, y), (self.width, y)], fill=(r, g, b)) # 添加纹理噪声 noise_intensity = self.cfg["bg_noise_intensity"] for _ in range(self.width * self.height // 8): x = rng.randint(0, self.width - 1) y = rng.randint(0, self.height - 1) pixel = img.getpixel((x, y)) noise = tuple( max(0, min(255, c + rng.randint(-noise_intensity, noise_intensity))) for c in pixel ) draw.point((x, y), fill=noise) # 随机色块 (模拟图案) for _ in range(rng.randint(3, 6)): x1, y1 = rng.randint(0, self.width - 30), rng.randint(0, self.height - 20) x2, y2 = x1 + rng.randint(15, 40), y1 + rng.randint(10, 25) color = tuple(rng.randint(60, 220) for _ in range(3)) draw.rectangle([x1, y1, x2, y2], fill=color) return img