29_PDF水印添加
摘要:在企业文档管理中,水印是保护版权、防止泄密的重要手段。无论是添加 “机密” 字样的文本水印,还是公司 Logo 的图片水印,都需要精准控制位置、透明度和层级。
本章将结合 reportlab(用于绘制水印)和 pypdf(用于合并图层),深入讲解如何在不破坏源文件内容的前提下,实现高性能的水印添加服务。我们将重点解决中文乱码、旋转坐标系变换以及批量处理等工程难题。
关联的小程序样例:薄荷百宝箱
29.1 水印文字添加
29.1.1 核心原理:图层叠加 (Overlay)
PDF 文件支持多层结构。添加水印的本质,是生成一个只包含水印内容的透明 PDF 页面,然后将其作为“印章”盖在源文件的每一页上。
我们需要两个库:
reportlab:用于动态生成包含水印的 PDF 流(Canvas)。pypdf:用于读取源文件,并将水印层与源页面合并(Merge)。29.1.2 基础实现
from io import BytesIO
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from pypdf import PdfReader, PdfWriter
def create_text_watermark(content: str) -> BytesIO:
"""
使用 ReportLab 生成纯水印 PDF
"""
buffer = BytesIO()
# 默认使用 A4,实际应根据源文件动态调整
c = canvas.Canvas(buffer, pagesize=A4)
# 设置字体和大小
c.setFont("Helvetica", 60)
# 绘制文字 (坐标 x=200, y=400, PDF 原点在左下角)
c.drawString(200, 400, content)
c.save()
buffer.seek(0)
return buffer
def add_watermark(input_stream: BytesIO, watermark_text: str) -> BytesIO:
# 1. 生成水印层
watermark_buffer = create_text_watermark(watermark_text)
watermark_reader = PdfReader(watermark_buffer)
watermark_page = watermark_reader.pages[0]
# 2. 读取源文件
reader = PdfReader(input_stream)
writer = PdfWriter()
# 3. 遍历每一页进行合并
for page in reader.pages:
# merge_page 会将水印层叠加在当前页之上
# ⚠️ 注意:这会修改 page 对象本身
page.merge_page(watermark_page)
writer.add_page(page)
output = BytesIO()
writer.write(output)
output.seek(0)
return output
常见坑:reportlab 默认不支持中文字体。如果直接写入 “机密文件”,会显示乱码。必须注册中文字体(见 29.3)。
29.2 水印图片添加
29.2.1 场景与实现
图片水印通常用于添加公司 Logo 或公章。逻辑与文字水印类似,只是在 Canvas 上绘制的是 Image。
def create_image_watermark(image_path: str) -> BytesIO:
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
# drawImage(image, x, y, width, height, mask='auto')
# mask='auto' 用于处理透明背景的 PNG
c.drawImage(image_path, 100, 500, width=100, height=100, mask='auto')
c.save()
buffer.seek(0)
return buffer
29.3 水印位置控制
29.3.1 坐标系与旋转
PDF 的坐标原点 (0, 0) 位于页面左下角。这与 Web 开发(左上角)不同。
如果我们想要实现“45度角平铺水印”,不能简单地旋转文字,而是要旋转坐标系。
from reportlab.lib.colors import Color
def create_tiled_watermark(
content: str,
width: float,
height: float
) -> BytesIO:
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=(width, height))
# 1. 注册中文字体 (前提:字体文件存在于系统或项目中)
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
# pdfmetrics.registerFont(TTFont('SimSun', 'SimSun.ttf'))
# c.setFont('SimSun', 30)
c.setFont("Helvetica", 30)
c.setFillColor(Color(0, 0, 0, alpha=0.1)) # 设置透明度
# 2. 变换坐标系
c.translate(width / 2, height / 2) # 移到中心
c.rotate(45) # 旋转 45 度
c.translate(-width, -height) # 移回(扩大绘制范围覆盖旋转后的空白)
# 3. 平铺绘制
# 在足够大的区域内循环绘制,确保旋转后能覆盖全页
for x in range(0, int(width * 2), 200):
for y in range(0, int(height * 2), 100):
c.drawString(x, y, content)
c.save()
buffer.seek(0)
return buffer
29.3.2 动态适配页面尺寸
源 PDF 可能包含 A4、A3 甚至横向页面混合的情况。如果用固定的 A4 水印层去盖 A3 页面,水印会偏。
最佳实践:针对每一页的尺寸动态生成水印。
def add_responsive_watermark(input_stream: BytesIO, text: str) -> BytesIO:
reader = PdfReader(input_stream)
writer = PdfWriter()
for page in reader.pages:
# 获取当前页宽高
page_width = float(page.mediabox.width)
page_height = float(page.mediabox.height)
# 针对该尺寸生成专属水印
wm_buffer = create_tiled_watermark(text, page_width, page_height)
wm_reader = PdfReader(wm_buffer)
wm_page = wm_reader.pages[0]
# 合并
page.merge_page(wm_page)
writer.add_page(page)
# ... save ...
29.4 水印透明度设置
29.4.1 Alpha 通道
水印绝对不能遮挡正文内容。我们需要设置 Alpha 透明度(0.0 完全透明 ~ 1.0 不透明)。
在 reportlab 中,这是通过 Color 对象控制的。
from reportlab.lib.colors import Color
# 灰色,透明度 0.2 (20%)
transparent_grey = Color(0.5, 0.5, 0.5, alpha=0.2)
c.setFillColor(transparent_grey)
# 或者仅设置描边颜色
c.setStrokeColor(transparent_grey)
29.4.2 层级问题 (Z-Index)
page.merge_page(watermark) 默认将水印放在最上层。 如果源文件包含不透明的背景图(如扫描件),水印放底层会被挡住;如果水印放顶层,透明度不够会影响阅读。
通常策略:
29.5 批量水印处理
29.5.1 并发与资源复用
如果要给 100 个文件加水印,每次都重新生成 watermark.pdf 是浪费的。
优化策略:
def batch_add_watermark(pdf_streams: list[BytesIO], text: str):
# 简单缓存:key="width-height", value=PageObject
watermark_cache = {}
for stream in pdf_streams:
reader = PdfReader(stream)
writer = PdfWriter()
for page in reader.pages:
w = int(page.mediabox.width)
h = int(page.mediabox.height)
key = f"{w}-{h}"
if key not in watermark_cache:
# 生成新水印并缓存
wm_buffer = create_tiled_watermark(text, w, h)
watermark_cache[key] = PdfReader(wm_buffer).pages[0]
# 复用缓存对象
page.merge_page(watermark_cache[key])
writer.add_page(page)
# ... save stream ...
本章面试题与知识点总结
1. reportlab 生成的 PDF 坐标原点在哪里?这与网页开发有什么不同?
参考答案:
●PDF 原点:reportlab遵循 PDF 标准,原点(0, 0)位于页面的左下角,Y 轴向上增长。●网页/图像:通常原点在左上角,Y 轴向下增长。●影响:在计算水印位置时,如果要把文字放在页面顶部,Y 坐标应该是PageHeight - Margin,而不是直接设为Margin。
2. 添加中文水印时出现乱码(方框),如何解决?
参考答案:
reportlab默认只内置了英文字体(如 Helvetica)。必须注册支持中文的 TrueType 字体(.ttf)。“`
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont必须确保 .ttf 文件路径正确
pdfmetrics.registerFont(TTFont(‘SimSun’, ‘/usr/share/fonts/simsun.ttf’))
c.setFont(‘SimSun’, 20)
“`在 Docker 容器中部署时,记得将字体文件 COPY 进去。
3. 如何实现“底层水印”(即水印在文字下方)?
参考答案:
pypdf的page.merge_page(watermark)默认是将watermark覆盖在page上(Overlay)。 要实现底层水印(Underlay),逻辑相反:1.创建一个空白的新页面new_page。2.将水印合并到new_page上:new_page.merge_page(watermark)。3.将源内容合并到new_page上:new_page.merge_page(source_page)。 注意:如果源 PDF 有白色不透明背景(常见于扫描件或 Word 转 PDF),底层水印会被完全遮挡,此时只能用顶层半透明水印。
夜雨聆风
