Java实现PDF批量处理之合并、拆分、加水印的技术方案与实践

 更新时间:2026年02月11日 09:36:07   作者:用户3891775626958  
在日常开发中,PDF文档处理是一个高频需求,无论是合同管理系统需要合并多份PDF,还是报表系统需要按页拆分,亦或是给机密文档添加水印,都需要一套可靠的PDF处理方案,本文将分享基于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文档的资料请关注脚本之家其它相关文章!

相关文章

  • Graceful Response 构建 Spring Boot 响应处理的方法

    Graceful Response 构建 Spring Boot 响应

    Graceful Response是一个Spring Boot技术栈下的优雅响应处理器,提供一站式统一返回值封装、全局异常处理、自定义异常错误码等功能,本文介绍Graceful Response 构建 Spring Boot 下优雅的响应处理,感兴趣的朋友一起看看吧
    2024-01-01
  • 详解Java泛型中类型擦除问题的解决方法

    详解Java泛型中类型擦除问题的解决方法

    Java泛型的实现是不完整的,有时会遇到一些Java泛型类型擦除的问题。本文将详细为大家讲解Java泛型中类型擦除问题的解决方法,需要的可以参考一下
    2022-05-05
  • IDEA 中创建SpringBoot 父子模块的实现

    IDEA 中创建SpringBoot 父子模块的实现

    这篇文章主要介绍了IDEA 中创建SpringBoot 父子模块的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Java后台开发之表单提交之前验证

    Java后台开发之表单提交之前验证

    这篇文章主要介绍了Java后台开发之表单提交之前验证的实现代码,非常不错具有参考借鉴价值,需要的朋友参考下吧
    2017-02-02
  • Java日期格式化实现过程

    Java日期格式化实现过程

    本文介绍了在Java中处理日期格式yyyyMMddHHmmss的方法,包括使用java.time包(Java8+)和SimpleDateFormat(Java7及以下),详细说明了格式化和解析时间的示例代码,并强调了线程安全和异常处理的重要性,还讨论了常见错误及其解决方法,以及如何生成唯一标识符
    2025-11-11
  • Idea中git的使用小结

    Idea中git的使用小结

    这篇文章主要介绍了Idea中git的使用小结,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-01-01
  • Java基础知识之Java语言概述

    Java基础知识之Java语言概述

    这篇文章主要介绍了Java基础知识之Java语言概述,本文介绍了Java语言相关的基础知识、历史介绍、主要应用方向等内容,需要的朋友可以参考下
    2015-03-03
  • 详解spring mvc4使用及json 日期转换解决方案

    详解spring mvc4使用及json 日期转换解决方案

    本篇文章主要介绍了spring mvc4使用及json 日期转换解决方案,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • Java8中Map常用的遍历方式

    Java8中Map常用的遍历方式

    这篇文章主要给大家介绍了关于Java8中Map常用的遍历方式,map属于java中的顶级接口之一,区别于list,map是键值对的形式存在,需要的朋友可以参考下
    2023-07-07
  • Java编码摘要算法实例解析

    Java编码摘要算法实例解析

    这篇文章主要介绍了Java编码摘要算法实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01

最新评论