Java实现PDF合并拯救仓库人员

643 阅读2分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

1 故事

故事是这样的,我们是一个商城系统,在输出快递单的时候,第三方给我们的一直是单个的面单。我们的仓库人员只能一张一张的打印,一张一张去贴。

效率低呀,还累。点一下打印出一张,点一下打印出一张。俺们就问第三方,你们有批量申请面单的接口吗?给到的回答是No。俺们就只能自己做了。其实这个不难,到也有不少坑。在就在这记个笔记,踩个脚印。

2 实现合并

集体的需求是这样的:

  1. 合并接口请求到的PDF
  2. 需要和传入参数顺序一样
  3. 输出到页面下载

2.1 代码示范

我先把代码留到这,然后一一解释

<dependency>
    <groupId>com.lowagie</groupId>
    <artifactId>itext</artifactId>
    <version>2.1.7</version>
</dependency>
private static volatile Map<String, Object>  readersMap = new HashMap<>();

@GetMapping("/pdf")
public void packingPrint(HttpServletResponse response) {
    //给定两个pdf的链接
    Map<String,String> map = new HashMap<>();
    map.put("1","file:///C:/Users/Administrator/Downloads/test1.pdf");
    map.put("2","file:///C:/Users/Administrator/Downloads/test2.pdf");
    mergePdfFiles(map,response);
}
private void mergePdfFiles(Map<String, String> map, HttpServletResponse response) {
    readersMap = new LinkedHashMap<>();
    readersMap.putAll(map);
    String now = LocalDateTime.now(ZoneId.of("Asia/Shanghai")).format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
    String filename = "合并后_" + now + ".pdf";
    try {
        //读取pdf对象
        CountDownLatch cdlatch = new CountDownLatch(map.entrySet().size());
        ExecutorService executorPool = ThreadPoolUtils.getExecutorPool(4);

        for (Map.Entry<String, String> stringEntry : map.entrySet()) {
            executorPool.execute(() -> {
                PdfReader reader = null;
                try {
                    reader = new PdfReader(stringEntry.getValue());
                    readersMap.put(stringEntry.getKey(),reader);
                    cdlatch.countDown();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        cdlatch.await();
        executorPool.shutdown();
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; fileName=" + new String(filename.getBytes("gb2312"), "ISO8859-1"));

        Document document = new Document();
        PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream());
        document.open();
        document.setPageSize(PageSize.A4.rotate());
        PdfContentByte cb = writer.getDirectContent();

        int pageOfCurrentReaderPDF = 0;
        List<PdfReader> reader = new ArrayList<>();
        for (Map.Entry<String, Object> stringObjectEntry : readersMap.entrySet()) {
            reader.add((PdfReader) stringObjectEntry.getValue());
        }

        Iterator<PdfReader> iteratorPDFReader = reader.iterator();

        while (iteratorPDFReader.hasNext()) {
            PdfReader pdfReader = iteratorPDFReader.next();

            while (pageOfCurrentReaderPDF < pdfReader.getNumberOfPages()) {
                document.newPage();//创建新的一页
                pageOfCurrentReaderPDF++;
                PdfImportedPage page = writer.getImportedPage(pdfReader, pageOfCurrentReaderPDF);
                cb.addTemplate(page, 0, 0);
            }
            pageOfCurrentReaderPDF = 0;
        }
        document.close();
        writer.close();

    } catch (IOException e) {
        throw new RuntimeException(e);
    } catch (DocumentException e) {
        throw new RuntimeException(e);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

3 解释

我挑重点内容解释一下。当然也包含我遇到的坑

3.1 性能

测试的时候发现,这一步是特别慢的。当30个面单合并的时候,竟然要15秒,这也太久了。

reader = new PdfReader(stringEntry.getValue());

这是在读取需要合并的pdf,是整个代码块最慢的地方,要提高效率咋办,只能多线程了呗。又必须等到所有的pdf读取完成了,才能执行合并的操作,这样的话就用CountDownLatch实现如下:大概意思就是CountDownLatch会有一个初始的值,这里我设置的是需要合并的pdf的数量,当有一个线程读取完成之后,则这个数量减一,当都读取完成之后,这个值为0的时候,主线程才会继续执行。

CountDownLatch cdlatch = new CountDownLatch(map.entrySet().size());
ExecutorService executorPool = ThreadPoolUtils.getExecutorPool(4);

for (Map.Entry<String, String> stringEntry : map.entrySet()) {
    executorPool.execute(() -> {
        PdfReader reader = null;
        try {
            reader = new PdfReader(stringEntry.getValue());
            readersMap.put(stringEntry.getKey(),reader);
            cdlatch.countDown();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    });
}
cdlatch.await();
executorPool.shutdown();

readersMap是个加了volatile的全局变量,保证了主线程可以访问到各个子线程读取之后的pdf数据。readersMap是个LinkedHashMap,保证了顺序。

3.2 输出文件的格式

response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; fileName=" + new String(filename.getBytes("gb2312"), "ISO8859-1"));

这两行里setContentType则规定了输出文件的格式为pdf,attachment表示为页面下载。fileName则为文件名,后边的参数则为中文显示提供了支持。

3.3 合并PDF

接下来就是紧张刺激的合并的过程了,整个过程没啥注意的,读取然后写入,如有需要照抄即可。

document.setPageSize(PageSize.A4.rotate());

值得注意的是setPageSize的时候,rotate()方法会改变纸张的方向,不需要转换方向,去掉即可。

4 效果

之前为两个文件

image.png

image.png

合并后为:

image.png