| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- # -*- coding: utf-8 -*-
- import base64
- import random
- import string
- import sys
- from io import BytesIO
- from pathlib import Path
- from typing import Tuple
- from PIL import Image, ImageDraw, ImageFont
- from app.config.setting import settings
- class CaptchaUtil:
- """
- 验证码工具类
- """
- @classmethod
- def _get_real_font_path(cls) -> str:
- """
- 私有方法:获取字体文件的真实路径(兼容开发/打包环境)
- - 开发环境:项目根目录下的相对路径
- - 打包环境:PyInstaller 临时目录下的路径
- """
- # 基础相对路径(从settings中获取)
- font_relative_path = settings.CAPTCHA_FONT_PATH
- if getattr(sys, 'frozen', False):
- # 打包后:拼接PyInstaller临时目录路径
- real_path = Path(sys._MEIPASS) / font_relative_path
- else:
- # 开发环境:拼接项目根目录路径(根据你的项目结构调整parent层级)
- # 假设当前文件路径:app/utils/captcha_util.py
- # 项目根目录 = 当前文件目录 → app/utils → app → 项目根
- real_path = Path(__file__).parent.parent.parent / font_relative_path
- # 验证路径是否存在,避免加载失败
- if not real_path.exists():
- raise FileNotFoundError(f"验证码字体文件不存在:{real_path}")
- return str(real_path)
- @classmethod
- def generate_captcha(cls) -> Tuple[str, str]:
- """
- 生成带有噪声和干扰的验证码图片(4位随机字符)。
- 返回:
- - Tuple[str, str]: [base64图片字符串, 验证码值]。
- """
- # 生成4位随机验证码
- chars = string.digits + string.ascii_letters
- captcha_value = ''.join(random.sample(chars, 4))
- # 创建一张随机颜色背景的图片
- width, height = 160, 60
- background_color = tuple(random.randint(230, 255) for _ in range(3))
- image = Image.new('RGB', (width, height), color=background_color)
- draw = ImageDraw.Draw(image)
- # ========== 修复:使用真实字体路径 ==========
- font = ImageFont.truetype(font=cls._get_real_font_path(), size=settings.CAPTCHA_FONT_SIZE)
- # 计算文本总宽度和高度
- total_width = sum(draw.textbbox((0, 0), char, font=font)[2] for char in captcha_value)
- text_height = draw.textbbox((0, 0), captcha_value[0], font=font)[3]
- # 计算起始位置,使文字居中
- x_start = (width - total_width) / 2
- y_start = (height - text_height) / 2 - draw.textbbox((0, 0), captcha_value[0], font=font)[1]
- # 绘制字符
- x = x_start
- for char in captcha_value:
- # 使用深色文字,增加对比度
- text_color = tuple(random.randint(0, 80) for _ in range(3))
- # 随机偏移,增加干扰
- x_offset = x + random.uniform(-2, 2)
- y_offset = y_start + random.uniform(-2, 2)
- # 绘制字符
- draw.text((x_offset, y_offset), char, font=font, fill=text_color)
- # 更新x坐标,增加字符间距的随机性
- x += draw.textbbox((0, 0), char, font=font)[2] + random.uniform(1, 5)
- # 添加干扰线
- for _ in range(4):
- line_color = tuple(random.randint(150, 200) for _ in range(3))
- points = [(i, int(random.uniform(0, height))) for i in range(0, width, 20)]
- draw.line(points, fill=line_color, width=1)
- # 添加随机噪点
- for _ in range(width * height // 60):
- point_color = tuple(random.randint(0, 255) for _ in range(3))
- draw.point(
- (random.randint(0, width), random.randint(0, height)),
- fill=point_color
- )
- # 将图像数据保存到内存中并转换为base64
- buffer = BytesIO()
- image.save(buffer, format='PNG', optimize=True)
- base64_string = base64.b64encode(buffer.getvalue()).decode()
- return base64_string, captcha_value
- @classmethod
- def captcha_arithmetic(cls) -> Tuple[str, int]:
- """
- 创建验证码图片(加减乘运算)。
- 返回:
- - Tuple[str, int]: [base64图片字符串, 计算结果]。
- """
- # 创建空白图像,使用随机浅色背景
- background_color = tuple(random.randint(230, 255) for _ in range(3))
- image = Image.new('RGB', (160, 60), color=background_color)
- draw = ImageDraw.Draw(image)
- # ========== 修复:使用真实字体路径 ==========
- font = ImageFont.truetype(font=cls._get_real_font_path(), size=settings.CAPTCHA_FONT_SIZE)
- # 生成运算数字和运算符
- operators = ['+', '-', '*']
- operator = random.choice(operators)
- # 对于减法,确保num1大于num2
- if operator == '-':
- num1 = random.randint(6, 10)
- num2 = random.randint(1, 5)
- else:
- num1 = random.randint(1, 9)
- num2 = random.randint(1, 9)
- # 计算结果
- result_map = {
- '+': lambda x, y: x + y,
- '-': lambda x, y: x - y,
- '*': lambda x, y: x * y
- }
- captcha_value = result_map[operator](num1, num2)
- # 绘制文本,使用深色增加对比度
- text = f'{num1} {operator} {num2} = ?'
- text_bbox = draw.textbbox((0, 0), text, font=font)
- text_width = text_bbox[2] - text_bbox[0]
- x = (160 - text_width) // 2
- draw.text((x, 15), text, fill=(0, 0, 139), font=font)
- # 添加干扰线
- for _ in range(3):
- line_color = tuple(random.randint(150, 200) for _ in range(3))
- draw.line([
- (random.randint(0, 160), random.randint(0, 60)),
- (random.randint(0, 160), random.randint(0, 60))
- ], fill=line_color, width=1)
- # 将图像数据保存到内存中并转换为base64
- buffer = BytesIO()
- image.save(buffer, format='PNG', optimize=True)
- base64_string = base64.b64encode(buffer.getvalue()).decode()
- return base64_string, captcha_value
|