""" 普通字符验证码生成器 生成风格: - 浅色随机背景 (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)