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

相关推荐
weixin_397574094 天前
PDF复杂表格的1:1还原引擎:跨页表格自动拼接技术实战
大数据·人工智能·pdf
Metaphor6924 天前
使用 Python 将 PDF 转换为 HTML
python·pdf·html
2601_961845154 天前
粉笔行测5000题电子版|pdf|解析
pdf·新媒体运营·github·个人开发·内容运营·规格说明书·极限编程
Sour4 天前
PDF翻译卡住不动怎么办?扫描件、OCR 和大文件排查清单
前端·pdf·ocr
狂奔solar4 天前
OpenDataLoader-PDF 做 PDF 解析可视化调试器
pdf·rag 预处理
chatexcel4 天前
ChatExcel Max使用教程:图片、PDF、网页与复杂Excel的一站式数据分析
数据分析·pdf·excel
绘梨衣5474 天前
PDF表格解析知识总结
开发语言·python·pdf
qq_546937274 天前
Excel批量转PDF_Word_图片,支持自动合并报表,效率翻倍。
pdf·word·excel
zyplayer-doc5 天前
企业知识库安全与权限管理完全指南:从加密到审计的六层防护
人工智能·安全·pdf·编辑器·创业创新
易鹤鹤.5 天前
pdf标注高亮
pdf