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

相关推荐
w2018009 小时前
申论答题纸模板大作文格子纸及行测答题卡PDF可打印
pdf
2401_8769641311 小时前
27考研396经济类联考历年真题PDF
考研·pdf
2401_8769641313 小时前
27唐迟阅读方法论|思维导图PDF
pdf
2401_8769641314 小时前
27唐迟长难句的逻辑PDF
pdf
Web打印15 小时前
HttpPrinter(web打印控件)的gridreport和Fastreport对 ☑、★、✓ 等 Unicode 符号的支持
chrome·pdf·web
Web打印16 小时前
HttpPrinter(web打印控件)的gridreport导出pdf,字体模糊的解决方法
pdf
Web打印18 小时前
2027年Web打印的几种方法
前端·pdf·web
庖丁AI19 小时前
PDF表格提取工具怎么选?普通转换、OCR和解析工具的区别
pdf·ocr
索西引擎19 小时前
【LangChain 1.0】 语义搜索实战:从 PDF 文档到向量知识库的完整 RAG 链路
langchain·pdf