推荐用 reportlab 生成单页水印 pdf 再用 pypdf 的 pdfwriter merge_page,避免 pypdf2 水印发虚;图像水印优先用 pil 的 alpha_composite 并确保 rgba 模式;多线程需隔离字体与绘图对象,路径统一用 pathlib.path 处理。

用 PdfWriter + reportlab 给 PDF 批量加水印,别碰 PyPDF2 的 merge_page
PDF 水印本质是往每页内容层之上叠一个带透明度的图形层,PyPDF2 的 merge_page 易丢矢量文字、不支持 alpha 通道,实测水印发虚或直接消失。推荐用 reportlab 生成水印 PDF,再用 PdfWriter(pypdf 3.0+)逐页 merge_page —— 稳、快、透明度可控。
实操建议:
- 水印 PDF 必须单页、尺寸与目标页完全一致(用
canvas.setPageSize((width, height))对齐),否则偏移或拉伸 -
pypdf的PdfWriter不修改原文件,必须显式调用write()写出新文件,漏掉就白跑 - 多线程下每个线程要新建独立的
PdfWriter实例,共用会触发RuntimeError: PdfWriter is not thread-safe - 示例关键片段:
from pypdf import PdfReader, PdfWriter<br>from reportlab.pdfgen import canvas<br>from reportlab.lib.colors import Color<br><br># 生成水印 PDF(单页)<br>c = canvas.Canvas("watermark.pdf", pagesize=(595, 842)) # A4<br>c.setFillColor(Color(0, 0, 0, alpha=0.1)) # 半透明黑<br>c.setFont("Helvetica", 60)<br>c.rotate(30)<br>c.drawCentredString(300, 400, "CONFIDENTIAL")<br>c.save()<br><br># 合并<br>reader = PdfReader("input.pdf")<br>writer = PdfWriter()<br>wmark = PdfReader("watermark.pdf").pages[0]<br>for page in reader.pages:<br> page.merge_page(wmark)<br> writer.add_page(page)<br>with open("output.pdf", "wb") as f:<br> writer.write(f)
给 PNG/JPEG/WebP 批量加水印,优先用 PIL.Image 的 alpha_composite,不是 paste
paste 默认忽略 alpha 通道,水印图哪怕带透明度,贴上去也变实心色块;而 alpha_composite 才真正做像素级 alpha 混合,能保留“半透明安全水印”的视觉效果。
实操建议:
- 水印图必须是 RGBA 模式,PNG 带透明背景即可;JPEG 不支持 alpha,强行读取会丢透明度,必须转成 RGBA:
watermark.convert("RGBA") - 主图如果是 RGB 模式,需先转 RGBA:
img = img.convert("RGBA"),否则alpha_composite报错 - 水印位置计算别硬写坐标,用
img.width // 2, img.height // 2居中,再加rotate变换更可靠 - WebP 支持 lossless 和 alpha,保存时加
save(..., lossless=True, quality=100)避免二次压缩失真
多线程批量处理时,threading.local() 是绕过 PIL 全局状态冲突的唯一靠谱方案
PIL 内部有未加锁的全局状态(比如字体缓存、图像模式默认值),多线程直接共用 ImageFont.truetype 或反复调用 convert 会概率性崩溃,报 RuntimeError: cannot create weak reference to 'XXX' object 或图像错乱。
实操建议:
- 每个线程必须持有自己独占的字体对象:
local_data = threading.local()<br>def get_font():<br> if not hasattr(local_data, 'font'):<br> local_data.font = ImageFont.truetype("simhei.ttf", 32)<br> return local_data.font - 不要在全局初始化字体或复用
ImageDraw.Draw实例,每次处理新图都重新ImageDraw.Draw(img) - 线程数别盲目设高,CPU 密集型任务(如图像缩放+叠加)超过
os.cpu_count()反而变慢,建议设为min(8, os.cpu_count())
Windows 下中文路径/文件名导致 OSError: [Errno 22] Invalid argument?统一用 pathlib.Path 处理路径
直接拼接字符串路径(如 "D:\data\水印\test.jpg")在 Windows 上容易因反斜杠转义或编码问题触发 OSError;pathlib 自动处理分隔符、编码和路径存在性检查,是跨平台最省心的方式。
实操建议:
- 所有路径操作用
Path:input_dir = Path("D:/data/水印"),list(input_dir.glob("*.png")) - 打开文件时传
str(path)给Image.open()或PdfReader(),别传Path对象本身(部分库不识别) - 检查路径是否存在再处理:
if not path.exists(): continue,避免批量时某个文件缺失导致整个线程退出
实际跑起来最常卡在水印图尺寸没对齐、中文字体路径不对、线程里复用了 PIL 全局对象这三处,其他都是小问题。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











