基于 PDFBox 的 PDF 水印管理:使用 OCG 层实现精准添加与一键去除

1. 前言

在企业级应用开发中,给 PDF 文件添加水印是常见的需求。然而,传统的加水印方式(直接在内容流中绘制文本)往往面临一个痛点:添加容易,去除难。由于 PDF 是指令式绘图,普通文本水印会与正文混杂,导致后期无法精准剔除。

本文将介绍一种利用 OCG (Optional Content Groups,可选内容组) 的方案。通过将水印放置在独立的"层"中,我们可以像使用 Photoshop 的图层一样,轻松实现水印的添加、隐藏与管理。

2. 环境依赖

本文使用 Apache PDFBox 2.0.19。请在 pom.xml 中添加以下依赖:

XML

<dependency>

<groupId>org.apache.pdfbox</groupId>

<artifactId>pdfbox</artifactId>

<version>2.0.19</version>

</dependency>

<dependency>

<groupId>org.apache.pdfbox</groupId>

<artifactId>pdfbox-tools</artifactId>

<version>2.0.19</version>

</dependency>

3. 技术核心:OCG (层)

OCG 允许我们将 PDF 内容划分为不同的组,并控制其可见性。

添加水印时:我们将水印指令包裹在 beginMarkedContent 和 endMarkedContent 之间,并关联到一个特定的 PDOptionalContentGroup 对象。

去除水印时:我们无需解析页面指令,只需在文档的 OCProperties 中找到对应的组名,将其状态设置为 disabled 即可。

4. 工具类实现代码

java 复制代码
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;

import java.io.File;
import java.io.IOException;

/**
 * PdfWatermarkManager
 * 基于 OCG 层技术实现的 PDF 水印管理工具类
 * 
 * @author YourName
 */
public class PdfWatermarkManager {

    // 定义唯一的层名称,用于后期精准定位和删除
    private static final String LAYER_ID = "Business_Watermark_Layer";

    /**
     * 添加 OCG 层水印
     * @param sourcePath 源文件路径
     * @param targetPath 生成文件路径
     * @param watermarkText 水印文字内容
     */
    public static void addLayerWatermark(String sourcePath, String targetPath, String watermarkText) throws IOException {
        try (PDDocument doc = PDDocument.load(new File(sourcePath))) {
            
            // 1. 获取或创建文档的层管理属性 (OCProperties)
            PDOptionalContentProperties ocProps = doc.getDocumentCatalog().getOCProperties();
            if (ocProps == null) {
                ocProps = new PDOptionalContentProperties();
                doc.getDocumentCatalog().setOCProperties(ocProps);
            }

            // 2. 注册我们的专属水印层
            PDOptionalContentGroup watermarkGroup;
            if (ocProps.hasGroup(LAYER_ID)) {
                watermarkGroup = ocProps.getGroup(LAYER_ID);
            } else {
                watermarkGroup = new PDOptionalContentGroup(LAYER_ID);
                ocProps.addGroup(watermarkGroup);
            }

            // 3. 遍历页面写入水印内容
            for (PDPage page : doc.getPages()) {
                // 使用 APPEND 模式,resetContext 设置为 true 以保证绘图状态隔离
                try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
                    
                    // --- 开始标记 OCG 内容 ---
                    cs.beginMarkedContent(COSName.OC, watermarkGroup);

                    // 设置绘图状态:透明度 0.3
                    PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
                    gs.setNonStrokingAlphaConstant(0.3f);
                    cs.setGraphicsStateParameters(gs);

                    // 绘制文字水印
                    cs.beginText();
                    cs.setFont(PDType1Font.HELVETICA_BOLD, 45);
                    cs.setNonStrokingColor(200, 200, 200); // 浅灰颜色
                    
                    // 坐标定位:通常放在页面中间
                    float width = page.getMediaBox().getWidth();
                    float height = page.getMediaBox().getHeight();
                    cs.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(45), width / 4, height / 4));
                    
                    cs.showText(watermarkText);
                    cs.endText();

                    // --- 结束标记 OCG 内容 ---
                    cs.endMarkedContent();
                }
            }
            doc.save(targetPath);
        }
    }

    /**
     * 去除 OCG 层水印 (逻辑去除)
     * @param sourcePath 带有 OCG 水印的文件路径
     * @param targetPath 处理后的文件路径
     */
    public static void removeLayerWatermark(String sourcePath, String targetPath) throws IOException {
        try (PDDocument doc = PDDocument.load(new File(sourcePath))) {
            PDOptionalContentProperties ocProps = doc.getDocumentCatalog().getOCProperties();
            
            if (ocProps != null) {
                // 遍历文档中的层名称,匹配并禁用
                for (String name : ocProps.getGroupNames()) {
                    if (LAYER_ID.equals(name)) {
                        ocProps.setGroupEnabled(name, false); // 禁用该层的显示与打印
                    }
                }
                doc.save(targetPath);
            }
        }
    }
}

5. 方案优势总结

非破坏性管理:传统的加水印是直接修改原始 ContentStream,容易导致 PDF 对象错乱。OCG 方案是在逻辑层进行标记,更加安全。

精准定位:通过 LAYER_ID 标识,我们可以确保只删除我们自己添加的水印,而不会误删 PDF 原有的图片或文本。

打印控制:OCG 层不仅可以控制屏幕显示,还可以通过配置决定水印是否在打印时显示。

性能高效:去水印操作只需修改文档字典属性,不需要重新扫描成千上万个字符 Token,速度极快。

相关推荐
海带紫菜菠萝汤1 天前
免费在线翻译器横评:2026年6款PDF翻译工具对比
人工智能·python·pdf
SunnyDays10111 天前
C# 加密和解密 PDF:设置密码、AES 加密及操作权限
pdf·c#·加密 pdf·解除 pdf 密码·pdf 权限设置·pdf 文档安全
SamChan901 天前
WebSocket实现PDF翻译进度实时推送:Go后端+React前端的完整方案
前端·websocket·pdf·c#·react·机器翻译
森叶2 天前
拆解 PDF 翻译引擎:表格纹丝不动、长语言自动缩放、图片文字 AI 重绘背后的工程
人工智能·pdf
AI创界者2 天前
MinerU v3.4.4 离线部署实战:PDF/PPT/图片一键转 Markdown,低至 4G 显存/纯 CPU 可用
pdf·powerpoint
cosinmz2 天前
图片转PDF:手机合同照片如何整理成规范PDF文件
pdf·pdf转换·图片转pdf·图片转pdf小程序
a1117762 天前
深入理解 AI Agent PDF(设计原理与工程实践 )
pdf
CCC65A8780D2C2 天前
PaddleOCR-VL-1.6图片PDF文档解析识别成文本工具,解压就能用,完全本地离线
pdf
桐桐桐2 天前
技术文档翻译实践:保留PDF表格与版式的几种方案
pdf·自动化
王莎莎-MinerU2 天前
别让 Agent 重读 PDF:文档解析要交付可缓存资产
网络·数据库·人工智能·驱动开发·缓存·pdf·ocr