直接用PIL加水印失败,是因为django-simple-captcha等库返回的是不可编辑的BytesIO字节流,而非PIL Image对象;必须重写generate_image()方法,在生成环节拦截原始图像、转为Image实例后添加水印,并确保水印不干扰OCR识别与校验一致性。

为什么直接用PIL加水印到Django验证码上会失败
因为默认的 django-simple-captcha 或 django-captcha 生成的图片是内存中 BytesIO 流,没落地为 PIL Image 对象,你没法直接调用 ImageDraw 或 ImageFont —— 拿到的只是 HTTP 响应体字节流,不是可编辑图像。
替换 captcha.image 的渲染逻辑:从 BytesIO 提取 PIL Image
关键是在验证码生成过程中拦截原始图像数据,转成 PIL.Image 实例再叠加水印。以 django-simple-captcha 为例,需重写其 CaptchaStore.generate_image() 方法:
- 继承
CaptchaStore,覆盖generate_image() - 调用父类方法拿到原始
BytesIO,用Image.open(io.BytesIO(buf.getvalue()))加载 - 用
ImageDraw.Draw(img).text()添加半透明文字水印(注意坐标偏移和字体大小) - 最后用
img.save(buf, format='PNG')写回缓冲区
示例片段:
from PIL import Image, ImageDraw, ImageFont
import io
<p>def generate_image(self):</p><h1>调用原逻辑生成基础图</h1><pre class="brush:php;toolbar:false;">buf = super().generate_image()
img = Image.open(io.BytesIO(buf.getvalue())).convert('RGBA')
watermark = Image.new('RGBA', img.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(watermark)
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 16)
draw.text((10, 10), 'demo', fill=(200, 200, 200, 80), font=font)
out = Image.alpha_composite(img, watermark)
buf = io.BytesIO()
out.convert('RGB').save(buf, format='PNG')
return buf字体路径和透明度控制是部署时最容易出错的点
本地开发能跑通,上线后常报 OSError: cannot open resource 或水印全黑/不可见,原因很具体:
-
ImageFont.truetype()的路径在 Linux 容器里往往不存在,别硬编码/Library/Fonts/...或C:/Windows/... - 水印 alpha 值设太高(如 255)会盖住验证码字符;太低(如 10)又看不见;建议 60–100 区间试
- 如果验证码背景是纯色,水印文字要用与背景反差大的颜色(比如白底配深灰),否则加了也像没加
不推荐在视图层用 response.content 做后处理
有人想在 CaptchaRefreshView 或自定义视图里读取响应体、PIL 处理、再返回新响应——这会破坏 captcha 的 session 绑定逻辑,导致验证始终失败。根本原因是 captcha 的校验依赖 CaptchaStore 中存储的原始 hash,而你修改了图片但没更新对应字段。
真正安全的做法只有一处:侵入生成环节,确保输出图像和存储的验证码文本严格对应,且水印不干扰 OCR 识别(避免斜切、旋转水印文字)。
字体、坐标、透明度这些参数得在测试环境反复调,不然用户一眼看不出水印,或者干脆输不对验证码。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











