返回工具列表

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

📅 2026-02-10 🔄 2026-08-10 更新 ✍️ 轻语API开放平台 ⏱️ 阅读约 12 分钟

在日常开发中,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 注意事项

四、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(如上千页),逐页操作可能较慢。可以考虑:

五、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处理看似简单,但在生产环境中需要关注内存控制、字体兼容、并发处理等问题。希望本文的实践经验能帮助你少踩一些坑。


如果觉得有帮助,欢迎点赞收藏。有问题可以在评论区交流。