在日常办公中,经常需要给合同、报表等PDF文件的最后一页加盖图片水印(如公章、校验章)。手动处理单文件效率低下,批量处理又面临格式兼容、水印定位变形、异常报错等问题。本文将基于Python实现批量PDF水印添加功能,支持水印位置精准控制、大小灵活调整,并针对常见报错提供解决方案,适用于各类办公场景。
一、核心需求与技术选型
1. 核心功能需求
- 批量读取指定目录下所有PDF文件,避免手动逐个处理;
- 仅在PDF最后一页添加图片水印(shuiyin.png),不影响其他页面;
- 支持水印位置精准定位(自定义水平、垂直偏移量);
- 支持水印大小调整,保持图片宽高比不变形;
- 自动生成输出目录,规范命名输出文件,不修改原始PDF;
- 兼容不规范PDF文件,避免解析报错中断批量任务。
2. 技术栈选型
结合功能需求与兼容性考虑,最终选型如下库组合(替换原有PyPDF2以解决解析异常):
- PyMuPDF(fitz) :替代PyPDF2,对畸形PDF兼容性更强,解析速度快,可有效避免非法字符报错;
- reportlab:用于绘制图片水印并生成临时PDF,支持透明通道(PNG透明水印生效);
- Pillow:获取水印图片原始宽高比,保证水印拉伸无变形;
- os/io:处理文件目录、路径及内存缓冲区,实现批量文件遍历与临时数据存储。
二、环境准备
通过pip安装所需依赖库,执行以下命令:
pip install PyMuPDF Pillow reportlab
依赖说明:
- PyMuPDF(fitz):版本建议≥1.23.0,保证PDF解析兼容性;
- Pillow:版本建议≥9.0.0,支持多种图片格式宽高比获取;
- reportlab:版本建议≥3.6.0,确保图片水印绘制与PDF合并正常。
三、完整实现代码
以下代码整合了批量处理、水印定位、大小调整、异常容错等功能,可直接复制使用,只需根据实际需求修改配置参数。
import fitz # PyMuPDF核心库
import os
from reportlab.pdfgen import canvas
import io
from PIL import Image
def add_image_watermark_to_last_page(pdf_path, watermark_image_path, output_pdf_path, shuiyin_x, shuiyin_y, shuiyin_width):
"""
单个PDF处理:给最后一页添加图片水印(支持定位、大小调整,无变形)
:param pdf_path: 原始PDF文件路径
:param watermark_image_path: 水印图片路径(shuiyin.png)
:param output_pdf_path: 带水印PDF输出路径
:param shuiyin_x: 水印左上角水平偏移量(向右为正,原点在页面左下角)
:param shuiyin_y: 水印左上角垂直偏移量(向上为正,原点在页面左下角)
:param shuiyin_width: 水印目标宽度(高度按宽高比自动计算,避免变形)
"""
# 1. 文件合法性校验
if not os.path.exists(pdf_path):
raise FileNotFoundError(f"原始PDF文件不存在:{pdf_path}")
if not os.path.exists(watermark_image_path):
raise FileNotFoundError(f"水印图片不存在:{watermark_image_path}")
# 2. 打开原始PDF并获取页面信息
doc = fitz.open(pdf_path)
total_pages = len(doc)
if total_pages == 0:
doc.close()
raise ValueError(f"原始PDF文件为空,无页面可添加水印:{pdf_path}")
# 3. 获取最后一页尺寸(用于适配水印大小与位置)
last_page = doc[total_pages - 1]
page_rect = last_page.rect # 页面矩形区域(x0, y0, x1, y1)
page_width = page_rect.width
page_height = page_rect.height
# 4. 计算水印大小(保持原始宽高比,避免变形)
with Image.open(watermark_image_path) as img:
img_original_width, img_original_height = img.size
img_aspect_ratio = img_original_height / img_original_width # 高/宽比例
watermark_height = shuiyin_width * img_aspect_ratio # 自动适配高度
# 5. 生成临时水印PDF(支持透明通道)
packet = io.BytesIO() # 内存缓冲区,避免生成临时文件
c = canvas.Canvas(packet, pagesize=(page_width, page_height))
# 绘制水印(定位、大小、透明通道均生效)
c.drawImage(
watermark_image_path,
shuiyin_x,
shuiyin_y,
width=shuiyin_width,
height=watermark_height,
mask='auto' # 启用PNG透明通道
)
c.save()
packet.seek(0) # 重置缓冲区指针至起始位置
# 6. 合并水印到PDF最后一页
watermark_doc = fitz.open("pdf", packet.read()) # 读取临时水印PDF
last_page.show_pdf_page(page_rect, watermark_doc, 0) # 合并水印层
# 7. 保存最终文件并关闭资源
doc.save(output_pdf_path)
doc.close()
watermark_doc.close()
print(f"✅ 处理完成:{output_pdf_path}(水印大小:{shuiyin_width:.1f}×{watermark_height:.1f},定位:x={shuiyin_x}, y={shuiyin_y})")
def batch_process_pdfs(input_dir, watermark_image_path, output_dir=None, shuiyin_x=100, shuiyin_y=100, shuiyin_width=200):
"""
批量处理PDF:遍历目录下所有PDF,统一添加水印
:param input_dir: 待处理PDF所在目录(默认当前目录)
:param output_dir: 带水印PDF输出目录(默认创建with_watermark子目录)
:param shuiyin_x/y/width: 全局水印配置(位置、大小)
"""
# 1. 初始化输出目录(不存在则自动创建)
if output_dir is None:
output_dir = os.path.join(input_dir, "with_watermark")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"📁 已创建输出目录:{output_dir}")
# 2. 筛选目录下所有PDF文件(排除输出目录内文件,避免重复处理)
pdf_files = []
for filename in os.listdir(input_dir):
if filename.lower().endswith(".pdf"):
pdf_file_path = os.path.join(input_dir, filename)
if not pdf_file_path.startswith(os.path.abspath(output_dir)):
pdf_files.append((filename, pdf_file_path))
# 3. 无PDF文件处理逻辑
if not pdf_files:
print("⚠️ 未在指定目录找到可处理的PDF文件")
return
# 4. 批量处理每个PDF(单个文件报错不中断整体任务)
print(f"🔍 共找到 {len(pdf_files)} 个待处理PDF文件,开始批量添加水印...")
print(f"🌐 全局水印配置:宽度={shuiyin_width},定位:x={shuiyin_x}, y={shuiyin_y}")
for filename, pdf_path in pdf_files:
# 输出文件名规范:原始文件名_with_watermark.pdf
output_filename = f"{os.path.splitext(filename)[0]}_with_watermark.pdf"
output_pdf_path = os.path.join(output_dir, output_filename)
try:
add_image_watermark_to_last_page(
pdf_path=pdf_path,
watermark_image_path=watermark_image_path,
output_pdf_path=output_pdf_path,
shuiyin_x=shuiyin_x,
shuiyin_y=shuiyin_y,
shuiyin_width=shuiyin_width
)
except Exception as e:
print(f"❌ 处理失败 {filename}:{str(e)}")
print(f"\n🎉 批量处理结束!所有带水印PDF已保存至:{output_dir}")
if __name__ == "__main__":
# -------------------------- 核心配置参数(按需修改)--------------------------
WATERMARK_IMAGE = "shuiyin.png" # 水印图片路径(与脚本同目录可直接填文件名)
INPUT_DIRECTORY = "." # 待处理PDF目录("."表示当前目录,可填绝对路径)
OUTPUT_DIRECTORY = None # 输出目录(默认创建with_watermark子目录)
SHUIYIN_X = 100 # 水印水平偏移量(向右为正)
SHUIYIN_Y = 100 # 水印垂直偏移量(向上为正)
SHUIYIN_WIDTH = 200 # 水印宽度(高度自动适配,控制水印大小)
# ----------------------------------------------------------------------------
# 执行批量处理
try:
batch_process_pdfs(
input_dir=INPUT_DIRECTORY,
watermark_image_path=WATERMARK_IMAGE,
output_dir=OUTPUT_DIRECTORY,
shuiyin_x=SHUIYIN_X,
shuiyin_y=SHUIYIN_Y,
shuiyin_width=SHUIYIN_WIDTH
)
except Exception as e:
print(f"🚨 批量处理程序异常终止:{str(e)}")
四、核心功能配置指南
脚本核心配置集中在if __name__ == "__main__":模块,可根据实际需求快速调整,无需修改核心逻辑。
1. 水印位置调整(shuiyin_x、shuiyin_y)
PDF页面坐标体系以左下角为原点(0,0) ,水平方向向右为正,垂直方向向上为正,单位为“点(pt)”(1英寸=72点,A4页面宽度约595pt、高度约842pt)。
- 左移水印:减小shuiyin_x(如shuiyin_x=50,靠近页面左侧);
- 右移水印:增大shuiyin_x(如shuiyin_x=300,靠近页面右侧);
- 下移水印:减小shuiyin_y(如shuiyin_y=50,靠近页面底部);
- 上移水印:增大shuiyin_y(如shuiyin_y=500,靠近页面顶部);
- 示例:若需将水印放在右下角,可结合页面宽度计算(需在add_image_watermark_to_last_page函数内调整):
# 右下角定位(右侧、底部各留100pt边距) `` shuiyin_x = page_width - shuiyin_width - 100 ``shuiyin_y = 100
2. 水印大小调整(shuiyin_width)
脚本通过控制水印宽度(shuiyin_width)实现大小调整,高度会根据图片原始宽高比自动计算,确保水印无拉伸变形,支持两种调整方案:
方案1:固定尺寸(适合统一尺寸PDF)
直接设置shuiyin_width为固定值,水印大小不随PDF页面变化,适合处理一批尺寸相同的PDF。
- 缩小水印:shuiyin_width=150(宽度150pt,高度自动适配);
- 放大水印:shuiyin_width=300(宽度300pt,高度自动适配)。
方案2:相对尺寸(适合多样尺寸PDF)
将水印宽度设置为PDF页面宽度的百分比,自适应不同尺寸PDF,保证水印显示比例一致。需修改add_image_watermark_to_last_page函数内的水印宽度计算逻辑:
# 替换原有watermark_height计算前的代码
watermark_ratio = 0.3 # 水印宽度为页面宽度的30%(可调整0.2~0.5)
shuiyin_width = page_width * watermark_ratio # 相对宽度
watermark_height = shuiyin_width * img_aspect_ratio # 自适应高度
3. 目录与文件配置
- 水印图片路径:若shuiyin.png与脚本不在同一目录,需填写绝对路径(如"D:/images/shuiyin.png");
- 待处理PDF目录:INPUT_DIRECTORY可填绝对路径(如"D:/pdfs/to_process"),批量处理该目录下所有PDF;
- 输出目录:OUTPUT_DIRECTORY可自定义绝对路径(如"D:/pdfs/processed"),默认在待处理目录下创建with_watermark子目录。
五、常见问题与解决方案
1. 报错:Illegal character in Name Object (b'/\xfeG~\xe0')
问题原因
原始PDF文件包含非标准命名对象、特殊字符或编码混乱的元数据,PyPDF2解析兼容性弱,触发非法字符校验报错。
解决方案
本文脚本已采用PyMuPDF(fitz)替代PyMuPDF,其对畸形PDF兼容性极强,可直接规避该错误。若仍使用PyPDF2,可通过修改源码临时解决:
- 执行
pip show PyPDF2获取库安装路径; - 打开安装路径下的PyPDF2/generic.py文件,找到NameObject类的__init__方法;
- 注释掉非法字符校验的异常抛出代码:
# 注释以下两行代码 `` # if not self.is_valid(name): ``# raise PdfReadError(f"Illegal character in Name Object ({self._name})")
2. 水印变形、模糊
问题原因
未保持图片原始宽高比,或水印大小设置超出图片本身分辨率。
解决方案
- 严格通过宽高比自动计算水印高度,不手动修改watermark_height;
- 确保shuiyin.png原始分辨率足够(建议≥300dpi),避免放大后模糊;
- 相对尺寸方案下,水印比例控制在0.2~0.5之间,兼顾清晰度与页面占比。
3. 水印透明通道失效(PNG透明背景变白色)
问题原因
reportlab绘制图片时未启用透明通道支持。
解决方案
确保drawImage方法中添加mask='auto'参数(本文脚本已包含),启用PNG透明通道解析:
c.drawImage(..., mask='auto')
4. 单个PDF处理失败,批量任务中断
问题原因
单个PDF文件损坏、路径错误或权限不足,未捕获异常导致整体任务终止。
解决方案
脚本已在批量处理循环中添加try-except捕获异常,单个文件处理失败会打印错误信息并跳过,不影响其他文件处理。
六、使用步骤总结
- 安装依赖库:执行
pip install PyMuPDF Pillow reportlab; - 准备文件:将脚本保存为batch_pdf_watermark.py,将shuiyin.png与待处理PDF放在同一目录(或修改脚本路径参数);
- 配置参数:根据需求调整脚本中的水印位置(shuiyin_x/y)、大小(shuiyin_width)及目录参数;
- 运行脚本:终端执行
python3 batch_pdf_watermark.py; - 查看结果:处理完成后,在输出目录(with_watermark)中获取带水印的PDF文件。