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