captcha_util.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # -*- coding: utf-8 -*-
  2. import base64
  3. import random
  4. import string
  5. import sys
  6. from io import BytesIO
  7. from pathlib import Path
  8. from typing import Tuple
  9. from PIL import Image, ImageDraw, ImageFont
  10. from app.config.setting import settings
  11. class CaptchaUtil:
  12. """
  13. 验证码工具类
  14. """
  15. @classmethod
  16. def _get_real_font_path(cls) -> str:
  17. """
  18. 私有方法:获取字体文件的真实路径(兼容开发/打包环境)
  19. - 开发环境:项目根目录下的相对路径
  20. - 打包环境:PyInstaller 临时目录下的路径
  21. """
  22. # 基础相对路径(从settings中获取)
  23. font_relative_path = settings.CAPTCHA_FONT_PATH
  24. if getattr(sys, 'frozen', False):
  25. # 打包后:拼接PyInstaller临时目录路径
  26. real_path = Path(sys._MEIPASS) / font_relative_path
  27. else:
  28. # 开发环境:拼接项目根目录路径(根据你的项目结构调整parent层级)
  29. # 假设当前文件路径:app/utils/captcha_util.py
  30. # 项目根目录 = 当前文件目录 → app/utils → app → 项目根
  31. real_path = Path(__file__).parent.parent.parent / font_relative_path
  32. # 验证路径是否存在,避免加载失败
  33. if not real_path.exists():
  34. raise FileNotFoundError(f"验证码字体文件不存在:{real_path}")
  35. return str(real_path)
  36. @classmethod
  37. def generate_captcha(cls) -> Tuple[str, str]:
  38. """
  39. 生成带有噪声和干扰的验证码图片(4位随机字符)。
  40. 返回:
  41. - Tuple[str, str]: [base64图片字符串, 验证码值]。
  42. """
  43. # 生成4位随机验证码
  44. chars = string.digits + string.ascii_letters
  45. captcha_value = ''.join(random.sample(chars, 4))
  46. # 创建一张随机颜色背景的图片
  47. width, height = 160, 60
  48. background_color = tuple(random.randint(230, 255) for _ in range(3))
  49. image = Image.new('RGB', (width, height), color=background_color)
  50. draw = ImageDraw.Draw(image)
  51. # ========== 修复:使用真实字体路径 ==========
  52. font = ImageFont.truetype(font=cls._get_real_font_path(), size=settings.CAPTCHA_FONT_SIZE)
  53. # 计算文本总宽度和高度
  54. total_width = sum(draw.textbbox((0, 0), char, font=font)[2] for char in captcha_value)
  55. text_height = draw.textbbox((0, 0), captcha_value[0], font=font)[3]
  56. # 计算起始位置,使文字居中
  57. x_start = (width - total_width) / 2
  58. y_start = (height - text_height) / 2 - draw.textbbox((0, 0), captcha_value[0], font=font)[1]
  59. # 绘制字符
  60. x = x_start
  61. for char in captcha_value:
  62. # 使用深色文字,增加对比度
  63. text_color = tuple(random.randint(0, 80) for _ in range(3))
  64. # 随机偏移,增加干扰
  65. x_offset = x + random.uniform(-2, 2)
  66. y_offset = y_start + random.uniform(-2, 2)
  67. # 绘制字符
  68. draw.text((x_offset, y_offset), char, font=font, fill=text_color)
  69. # 更新x坐标,增加字符间距的随机性
  70. x += draw.textbbox((0, 0), char, font=font)[2] + random.uniform(1, 5)
  71. # 添加干扰线
  72. for _ in range(4):
  73. line_color = tuple(random.randint(150, 200) for _ in range(3))
  74. points = [(i, int(random.uniform(0, height))) for i in range(0, width, 20)]
  75. draw.line(points, fill=line_color, width=1)
  76. # 添加随机噪点
  77. for _ in range(width * height // 60):
  78. point_color = tuple(random.randint(0, 255) for _ in range(3))
  79. draw.point(
  80. (random.randint(0, width), random.randint(0, height)),
  81. fill=point_color
  82. )
  83. # 将图像数据保存到内存中并转换为base64
  84. buffer = BytesIO()
  85. image.save(buffer, format='PNG', optimize=True)
  86. base64_string = base64.b64encode(buffer.getvalue()).decode()
  87. return base64_string, captcha_value
  88. @classmethod
  89. def captcha_arithmetic(cls) -> Tuple[str, int]:
  90. """
  91. 创建验证码图片(加减乘运算)。
  92. 返回:
  93. - Tuple[str, int]: [base64图片字符串, 计算结果]。
  94. """
  95. # 创建空白图像,使用随机浅色背景
  96. background_color = tuple(random.randint(230, 255) for _ in range(3))
  97. image = Image.new('RGB', (160, 60), color=background_color)
  98. draw = ImageDraw.Draw(image)
  99. # ========== 修复:使用真实字体路径 ==========
  100. font = ImageFont.truetype(font=cls._get_real_font_path(), size=settings.CAPTCHA_FONT_SIZE)
  101. # 生成运算数字和运算符
  102. operators = ['+', '-', '*']
  103. operator = random.choice(operators)
  104. # 对于减法,确保num1大于num2
  105. if operator == '-':
  106. num1 = random.randint(6, 10)
  107. num2 = random.randint(1, 5)
  108. else:
  109. num1 = random.randint(1, 9)
  110. num2 = random.randint(1, 9)
  111. # 计算结果
  112. result_map = {
  113. '+': lambda x, y: x + y,
  114. '-': lambda x, y: x - y,
  115. '*': lambda x, y: x * y
  116. }
  117. captcha_value = result_map[operator](num1, num2)
  118. # 绘制文本,使用深色增加对比度
  119. text = f'{num1} {operator} {num2} = ?'
  120. text_bbox = draw.textbbox((0, 0), text, font=font)
  121. text_width = text_bbox[2] - text_bbox[0]
  122. x = (160 - text_width) // 2
  123. draw.text((x, 15), text, fill=(0, 0, 139), font=font)
  124. # 添加干扰线
  125. for _ in range(3):
  126. line_color = tuple(random.randint(150, 200) for _ in range(3))
  127. draw.line([
  128. (random.randint(0, 160), random.randint(0, 60)),
  129. (random.randint(0, 160), random.randint(0, 60))
  130. ], fill=line_color, width=1)
  131. # 将图像数据保存到内存中并转换为base64
  132. buffer = BytesIO()
  133. image.save(buffer, format='PNG', optimize=True)
  134. base64_string = base64.b64encode(buffer.getvalue()).decode()
  135. return base64_string, captcha_value