Java实现PDF批量处理之合并、拆分、加水印的技术方案与实践
一、技术选型
Java生态中处理PDF的库主要有以下几个:
| 库名 | 特点 | 适用场景 |
|------|------|----------|
| Apache PDFBox | 开源免费,功能全面 | 合并、拆分、水印、文本提取 |
| iText | 功能强大,商用需授权 | 复杂PDF生成、表单处理 |
| OpenPDF | iText的开源分支 | 轻量级PDF生成 |
综合考虑开源协议和功能覆盖度,Apache PDFBox 是大多数项目的首选。
二、Maven依赖配置
<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.27</version> </dependency>
三、PDF合并实现
3.1 核心思路
PDF合并的本质是将多个PDF文档的页面按顺序追加到一个新文档中。PDFBox提供了PDFMergerUtility工具类来简化这个过程。
3.2 代码实现
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import java.io.*;
import java.util.List;
public class PdfMergeService {
/**
* 合并多个PDF文件
* @param inputStreams PDF文件输入流列表
* @param outputPath 输出文件路径
*/
public void mergePdf(List<InputStream> inputStreams, String outputPath)
throws IOException {
PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(outputPath);
for (InputStream is : inputStreams) {
merger.addSource(is);
}
// 执行合并,MemoryUsageSetting可控制内存使用
merger.mergeDocuments(
org.apache.pdfbox.io.MemoryUsageSetting.setupMainMemoryOnly()
);
}
}
3.3 注意事项
- 内存管理:处理大文件时建议使用
setupTempFileOnly()将临时数据写入磁盘,避免OOM - 文件顺序:合并顺序取决于
addSource的调用顺序,前端可通过拖拽排序来控制 - 书签处理:合并后原有书签可能丢失,如需保留需额外处理
PDDocumentOutline
四、PDF拆分实现
4.1 按页码范围拆分
import org.apache.pdfbox.multipdf.Splitter;
import org.apache.pdfbox.pdmodel.PDDocument;
public class PdfSplitService {
/**
* 按页码范围拆分PDF
* @param inputStream 源PDF输入流
* @param startPage 起始页(从1开始)
* @param endPage 结束页
*/
public byte[] splitByPageRange(InputStream inputStream,
int startPage, int endPage) throws IOException {
try (PDDocument document = PDDocument.load(inputStream);
PDDocument newDoc = new PDDocument()) {
int totalPages = document.getNumberOfPages();
// 参数校验
startPage = Math.max(1, startPage);
endPage = Math.min(totalPages, endPage);
for (int i = startPage - 1; i < endPage; i++) {
newDoc.addPage(document.getPage(i));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDoc.save(baos);
return baos.toByteArray();
}
}
}
4.2 性能优化建议
对于页数很多的PDF(如上千页),逐页操作可能较慢。可以考虑:
- 使用
Splitter类的setSplitAtPage()方法按固定页数批量拆分 - 对于仅需提取少量页面的场景,直接操作页面树比使用Splitter更高效
五、PDF加水印实现
5.1 文字水印核心代码
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;
public class PdfWatermarkService {
public byte[] addTextWatermark(InputStream input, String text,
float opacity, float rotation, float fontSize) throws IOException {
try (PDDocument document = PDDocument.load(input)) {
// 遍历每一页添加水印
for (PDPage page : document.getPages()) {
PDPageContentStream cs = new PDPageContentStream(
document, page,
PDPageContentStream.AppendMode.APPEND, true, true
);
// 设置透明度
PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
gs.setNonStrokingAlphaConstant(opacity);
cs.setGraphicsStateParameters(gs);
// 设置字体和颜色
cs.setFont(PDType1Font.HELVETICA_BOLD, fontSize);
cs.setNonStrokingColor(200, 200, 200); // 浅灰色
// 计算页面中心位置
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
float cx = pageWidth / 2;
float cy = pageHeight / 2;
// 旋转变换
cs.beginText();
Matrix matrix = Matrix.getRotateInstance(
Math.toRadians(rotation), cx, cy
);
cs.setTextMatrix(matrix);
cs.showText(text);
cs.endText();
cs.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
return baos.toByteArray();
}
}
}
5.2 中文水印的坑
PDFBox默认的PDType1Font不支持中文字符,直接使用会导致乱码或报错。解决方案:
// 加载系统中文字体
PDType0Font chineseFont = PDType0Font.load(document,
new FileInputStream("/usr/share/fonts/wqy-microhei/wqy-microhei.ttc"), 0);
cs.setFont(chineseFont, fontSize);
服务器部署时需确保安装了中文字体:
# CentOS/RHEL sudo yum install -y wqy-microhei-fonts # Ubuntu/Debian sudo apt-get install -y fonts-wqy-microhei
六、封装为REST API
在Spring Boot项目中,可以将上述功能封装为统一的REST接口:
@RestController
@RequestMapping("/api/document/pdf")
public class PdfController {
@PostMapping("/merge")
public ResponseEntity<?> merge(
@RequestParam("files") MultipartFile[] files) {
// 调用合并服务
}
@PostMapping("/split")
public ResponseEntity<?> split(
@RequestParam("file") MultipartFile file,
@RequestParam("startPage") int startPage,
@RequestParam("endPage") int endPage) {
// 调用拆分服务
}
@PostMapping("/watermark")
public ResponseEntity<?> watermark(
@RequestParam("file") MultipartFile file,
@RequestParam("text") String text,
@RequestParam(defaultValue = "0.5") float opacity) {
// 调用水印服务
}
}
七、实际效果
如果你不想自己搭建服务,也可以直接使用现成的在线工具来处理PDF。比如 轻语API开放平台 提供了免费的 PDF在线处理工具,支持合并、拆分、加水印、转Word、转图片等功能,底层就是基于上述技术方案实现的。
对于需要批量处理的场景,也可以通过API接口集成到自己的系统中,省去重复造轮子的成本。
八、总结
| 功能 | 核心类/方法 | 难点 |
|------|------------|------|
| PDF合并 | PDFMergerUtility | 大文件内存管理 |
| PDF拆分 | PDDocument.getPage() | 页码边界校验 |
| PDF水印 | PDPageContentStream | 中文字体支持 |
PDF处理看似简单,但在生产环境中需要关注内存控制、字体兼容、并发处理等问题。希望本文的实践经验能帮助你少踩一些坑。
以上就是Java实现PDF批量处理之合并、拆分、加水印的技术方案与实践的详细内容,更多关于Java批量处理PDF文档的资料请关注脚本之家其它相关文章!
相关文章
springboot整合webservice使用简单案例总结
WebService是一个SOA(面向服务的编程)的架构,它是不依赖于语言,平台等,可以实现不同的语言间的相互调用,下面这篇文章主要给大家介绍了关于springboot整合webservice使用的相关资料,需要的朋友可以参考下2024-07-07
SpringBoot Redis 启动失败深度剖析与标准化解决方案(快速定位问题)
本文基于SpringBoot 2.x/3.x(含Jakarta EE适配)与Redis 6.x/7.x生态,系统梳理12类常见错误,涵盖错误现象、深层原因、解决方案及最佳实践,助力开发者快速定位问题,感兴趣的朋友跟随小编一起看看吧2025-11-11


最新评论