Java 实现 PDF 文件添加文字 / 图片水印(附 MinIO 下载场景实战)
在日常开发中,PDF 加水印是非常常见的需求,比如文件脱敏、版权标识等场景。本文将基于 iTextPDF 库实现 PDF 文件的文字水印和图片水印添加,并结合 MinIO 文件下载场景,实现下载 PDF 时自动添加水印的实战功能。
一、技术选型与依赖配置
1. 核心依赖
本文使用iTextPDF作为 PDF 处理核心库,该库是 Java 生态中处理 PDF 的经典工具,支持 PDF 的创建、修改、水印添加等几乎所有操作。
Maven 依赖配置
<!-- iTextPDF 核心依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<!-- 中文字体支持 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<!-- MinIO客户端依赖(如果用到MinIO下载场景) -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!-- Apache Commons IO 工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>cy>
Gradle 依赖配置
// iTextPDF 核心
implementation 'com.itextpdf:itextpdf:5.5.13.3'
// 中文支持
implementation 'com.itextpdf:itext-asian:5.2.0'
// MinIO
implementation 'io.minio:minio:8.5.7'
// Commons IO
implementation 'commons-io:commons-io:2.15.1'
注意:iTextPDF 5.x 版本为经典稳定版,兼容大部分项目;如果使用 6.x + 版本,包名和部分 API 会有调整,需注意适配。
二、核心工具类实现(PDFUtil)
封装通用的 PDF 水印添加工具类,支持文字水印和图片水印两种方式,适配 Windows/Linux 系统的字体差异。
package com.tydt.util;
import com.itextpdf.text.Element;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.*;
import java.io.File;
import java.io.FileOutputStream;
/**
* PDF添加水印工具类
* 支持文字水印、图片水印两种方式
*/
public class PDFUtil {
/**
* PDF添加图片水印
* @param srcPath 源PDF文件路径
* @param destPath 加水印后PDF输出路径
* @param imagePath 水印图片路径
* @throws Exception 处理异常
*/
public static void addPDFImageWaterMark(String srcPath, String destPath, String imagePath) throws Exception {
// 1. 读取源PDF文件
PdfReader reader = new PdfReader(srcPath);
// 2. 创建PDF Stamper(用于修改PDF)
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(destPath));
// 3. 加载水印图片
Image image = Image.getInstance(imagePath);
// 4. 设置水印透明度
PdfGState gs = new PdfGState();
gs.setFillOpacity(0.2f); // 透明度0.2(0-1,值越小越淡)
PdfContentByte content = null;
// 5. 遍历PDF所有页面添加水印
int totalPages = reader.getNumberOfPages();
for (int i = 0; i < totalPages; i++) {
// 获取当前页面尺寸
float pageWidth = reader.getPageSize(i + 1).getWidth();
float pageHeight = reader.getPageSize(i + 1).getHeight();
// 获取页面的上层内容(水印需放在上层)
content = stamper.getOverContent(i + 1);
content.setGState(gs); // 应用透明度
// 循环添加水印(每页3列7行,均匀分布)
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 7; k++) {
// 设置图片水印位置
image.setAbsolutePosition(pageWidth / 3 * j - 30, pageHeight / 7 * k - 20);
content.addImage(image);
}
}
}
// 6. 关闭资源
stamper.close();
reader.close();
}
/**
* PDF添加文字水印
* @param srcPath 源PDF文件路径
* @param destPath 加水印后PDF输出路径
* @param waterMarkText 水印文字内容
* @throws Exception 处理异常
*/
public static void addPDFWaterMark(String srcPath, String destPath, String waterMarkText) throws Exception {
// 1. 读取源PDF文件
PdfReader reader = new PdfReader(srcPath);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(destPath));
// 2. 适配Windows/Linux系统字体(解决中文乱码)
String fontPath = null;
String osName = System.getProperties().getProperty("os.name");
if (osName.startsWith("win") || osName.startsWith("Win")) {
// Windows系统宋体
fontPath = "C:WindowsFontsSIMSUN.TTC,1";
} else {
// Linux系统中文字体(需提前安装中文字体)
fontPath = "/usr/share/fonts/chinese/TrueType/uming.ttf";
}
// 3. 创建基础字体(支持中文)
BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// 4. 设置水印透明度
PdfGState gs = new PdfGState();
gs.setFillOpacity(0.2f); // 文字透明度
PdfContentByte content = null;
// 5. 遍历所有页面添加水印
int totalPages = reader.getNumberOfPages();
for (int i = 0; i < totalPages; i++) {
float pageWidth = reader.getPageSize(i + 1).getWidth();
float pageHeight = reader.getPageSize(i + 1).getHeight();
content = stamper.getOverContent(i + 1);
content.setGState(gs);
content.beginText();
content.setFontAndSize(baseFont, 20); // 设置字体和字号
// 循环添加水印(每页3列7行,旋转25度,避免遮挡内容)
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 7; k++) {
// 参数说明:对齐方式、文字内容、X坐标、Y坐标、旋转角度
content.showTextAligned(Element.ALIGN_CENTER, waterMarkText,
pageWidth / 3 * j + 90, pageHeight / 7 * k, 25);
}
}
content.endText();
}
// 6. 关闭资源
stamper.close();
reader.close();
}
// 测试方法
public static void main(String[] args) {
// 批量给指定目录的PDF添加文字水印
File pdfDir = new File("D:wps");
for (File pdfFile : pdfDir.listFiles()) {
if (pdfFile.getName().endsWith(".pdf")) {
try {
System.out.println("正在处理:" + pdfFile.getName());
String srcPath = "D:wps" + pdfFile.getName();
String destPath = "D:wpswaterMark" + pdfFile.getName();
// 添加文字水印
addPDFWaterMark(srcPath, destPath, "天津港项目有限公司");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
关键代码说明
- 字体适配:区分 Windows 和 Linux 系统的中文字体路径,解决中文水印乱码问题;
- 透明度设置:通过PdfGState的setFillOpacity设置水印透明度,0.2 为最佳视觉效果;
- 水印分布:按页面尺寸均分,每页 3 列 7 行添加水印,保证全屏覆盖且不密集;
- 文字旋转:设置 25 度旋转角,避免水印与正文文字平行,提升视觉体验。
三、实战场景:MinIO 下载 PDF 自动加水印
在实际项目中,经常需要从 MinIO 下载文件时,对 PDF 文件自动添加水印后再返回给前端。以下是完整的接口实现:
import io.minio.StatObjectResponse;
import org.apache.commons.io.IOUtils;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.InputStream;
import java.net.URLEncoder;
/**
* 通用文件下载接口(PDF自动加水印)
*/
@RestController
@RequestMapping("/file")
public class FileDownloadController {
@Autowired
private MinioTemplate minioTemplate;
@Autowired
private MinioConfig minioConfig;
@Autowired
private FileService fileService;
// 临时文件夹(需提前创建,建议配置在application.yml)
private String destFolder = "/tmp/pdf_watermark";
// 水印文字常量
private static final String WATER_MARK = "天津港项目有限公司";
/**
* 通用附件下载
* @param fileId 文件ID
* @param response 响应对象
* @throws Exception 处理异常
*/
@ApiOperation("通用附件下载")
@GetMapping("/download")
@ApiImplicitParams({
@ApiImplicitParam(name = "fileId", value = "文件ID", required = true)
})
public void download(String fileId, HttpServletResponse response) throws Exception {
// 1. 校验MinIO桶是否存在
String bucketName = minioConfig.getBucketName();
boolean bucketExists = minioTemplate.bucketExists(bucketName);
Assert.isTrue(bucketExists, "文件存储桶不存在!");
// 2. 查询文件信息
UploadFilePO uploadFilePo = fileService.queryFileDetail(fileId);
Assert.notNull(uploadFilePo, "文件不存在或已被删除!");
// 3. 获取MinIO文件元信息
StatObjectResponse statObject = minioTemplate.getStatObjectResponse(
bucketName, uploadFilePo.getFilePath(), uploadFilePo.getVersionId());
// 4. 设置响应头(文件下载)
response.setContentType(statObject.contentType());
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(uploadFilePo.getRealName(), "UTF-8"));
// 5. 区分PDF和非PDF文件处理
if ("pdf".equals(uploadFilePo.getFileType())) {
// 5.1 从MinIO获取PDF文件输入流
InputStream inputStream = minioTemplate.getObject(
bucketName, uploadFilePo.getFilePath(), uploadFilePo.getVersionId());
// 5.2 创建临时文件(避免并发冲突,使用雪花ID命名)
String tempDir = destFolder + File.separator + IdUtils.snowflakeNewId();
File tempDirFile = new File(tempDir);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs(); // 创建多级目录
}
String tempSrcPdf = tempDir + File.separator + uploadFilePo.getFileName();
String tempDestPdf = tempDir + File.separator + uploadFilePo.getRealName();
// 5.3 将MinIO的PDF写入临时文件
org.apache.commons.io.FileUtils.copyToFile(inputStream, new File(tempSrcPdf));
// 5.4 给PDF添加水印
PDFUtil.addPDFWaterMark(tempSrcPdf, tempDestPdf, WATER_MARK);
// 5.5 将加水印后的PDF写入响应流
FileUtils.writeBytes(tempDestPdf, response.getOutputStream());
// 5.6 删除临时文件(避免磁盘占用)
org.apache.commons.io.FileUtils.forceDelete(tempDirFile);
} else {
// 非PDF文件直接下载
InputStream inputStream = minioTemplate.getObject(
bucketName, uploadFilePo.getFilePath(), uploadFilePo.getVersionId());
IOUtils.copy(inputStream, response.getOutputStream());
}
}
}
实战要点
- 临时文件处理:使用雪花 ID 创建唯一临时目录,避免多线程下载时文件冲突;
- 资源释放:下载完成后强制删除临时文件,防止磁盘空间泄露;
- 响应头设置:通过URLEncoder.encode处理文件名,解决中文文件名乱码;
- 异常处理:实际项目中需增加 try-catch,保证临时文件无论是否异常都能删除。
四、常见问题与解决方案
Linux 系统中文乱码:
原因:Linux 默认无中文字体;
解决:安装fonts-wqy-microhei或fonts-chinese字体包,修改工具类中的字体路径。
水印覆盖 PDF 内容:
原因:使用getOverContent(上层内容),如果 PDF 有表单 / 批注可能被遮挡;
解决:改用getUnderContent(下层内容),水印放在文字下方。
iTextPDF 版本冲突:
原因:项目中其他依赖引入了低版本 iTextPDF;
解决:在 pom.xml 中排除冲突依赖,指定固定版本。
临时文件删除失败:
原因:文件流未关闭;
解决:在forceDelete前关闭所有流,或使用FileDeleteStrategy.FORCE强制删除。
五、总结
本文基于 iTextPDF 实现了 PDF 文字 / 图片水印的通用工具类,并结合 MinIO 文件下载场景完成了实战落地,核心要点如下:
- 依赖配置:核心引入 iTextPDF 和中文支持包,解决基础依赖;
- 工具类封装:适配多系统字体、控制水印透明度和分布,保证通用性;
- 实战落地:下载 PDF 时通过临时文件中转,加水印后返回并清理临时文件;
- 问题兜底:针对乱码、资源泄露、并发冲突等问题提供解决方案。
该方案已在实际项目中验证,可直接复用,适用于企业级 PDF 文件版权保护、脱敏等场景。








