乐于分享
好东西不私藏

29_PDF水印添加

29_PDF水印添加

摘要:在企业文档管理中,水印是保护版权、防止泄密的重要手段。无论是添加 “机密” 字样的文本水印,还是公司 Logo 的图片水印,都需要精准控制位置、透明度和层级。

本章将结合 reportlab(用于绘制水印)和 pypdf(用于合并图层),深入讲解如何在不破坏源文件内容的前提下,实现高性能的水印添加服务。我们将重点解决中文乱码、旋转坐标系变换以及批量处理等工程难题。

关联的小程序样例:薄荷百宝箱

29.1 水印文字添加

29.1.1 核心原理:图层叠加 (Overlay)

PDF 文件支持多层结构。添加水印的本质,是生成一个只包含水印内容的透明 PDF 页面,然后将其作为“印章”盖在源文件的每一页上。

我们需要两个库:

1.reportlab:用于动态生成包含水印的 PDF 流(Canvas)。
2.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) 默认将水印放在最上层。 如果源文件包含不透明的背景图(如扫描件),水印放底层会被挡住;如果水印放顶层,透明度不够会影响阅读。

通常策略

电子文档:水印放顶层,透明度 0.1~0.2。
扫描件:水印放顶层,透明度 0.3~0.5(因为背景杂乱,太淡看不清)。

29.5 批量水印处理

29.5.1 并发与资源复用

如果要给 100 个文件加水印,每次都重新生成 watermark.pdf 是浪费的。

优化策略

1.缓存水印层:如果源文件页面尺寸统一(如都是 A4),只生成一次水印 PDF,加载到内存中反复使用。
2.分组处理:按页面尺寸分组,同尺寸的页面复用同一个水印对象。
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. 如何实现“底层水印”(即水印在文字下方)?

参考答案pypdfpage.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),底层水印会被完全遮挡,此时只能用顶层半透明水印。
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 29_PDF水印添加

评论 抢沙发

7 + 7 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮