基于 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,速度极快。

相关推荐
tanis_20773 小时前
学术论文 PDF 的版面自动还原:MinerU 对多栏排版、浮动图表与脚注区域的识别实战
人工智能·pdf·ocr
tanis_20774 小时前
从 PDF 中精准提取表格、图片与公式:MinerU 结构化元素抽取的 3 种方案
pdf
Eric.Lee20215 小时前
python实现多个pdf合并
开发语言·python·pdf·pdf合并
寻道模式7 小时前
【开发心得】给私有部署OpenClaw添加PDF阅读技能
开发语言·python·pdf
优化控制仿真模型7 小时前
【2026六级】英语六级历年真题及答案PDF电子版(2015-2025年12月)
经验分享·pdf
南风微微吹7 小时前
初级银行从业资格考试历年真题及答案解析电子版pdf(2017-2025年)
pdf
非凡ghost8 小时前
完美解码最新版(完美解码播放器)
windows·智能手机·pdf·firefox·软件需求
复园电子8 小时前
开发者指南:PDF怎么自动生成骑缝章?电子签章底层技术与API集成方案
pdf