【Java实战】iText赋能:滴滴发票与行程单智能合并与打印优化方案
1. 为什么我们需要合并滴滴发票和行程单?
每次出差或加班打车后,财务报销总是个麻烦事。滴滴出行会生成两个PDF文件——电子发票和行程单,分开打印不仅浪费纸张,还容易丢失。更糟的是,很多PDF合并工具要么收费,要么有水印,要么操作复杂。作为经常需要报销的Java开发者,我决定用iText这个强大的PDF处理库自己动手解决这个问题。
iText是Java生态中最成熟的PDF处理库之一,它不仅能生成PDF,还能对现有PDF进行各种操作。实测下来,用iText处理滴滴的发票和行程单合并,效果比市面上大多数工具都好。最关键的是,整个过程完全自动化,再也不用手动调整页面大小和位置了。
2. iText环境准备与基础配置
2.1 引入iText依赖
首先需要在项目中加入iText依赖。如果你使用Maven,在pom.xml中添加:
<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13.3</version> </dependency> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.13.3</version> </dependency>建议使用5.x版本,因为这个版本稳定且文档丰富。新版本虽然功能更多,但API变化较大,学习成本高。
2.2 准备空白PDF模板
我们需要一个空白A4大小的PDF作为合并后的容器。这个技巧很实用——先在Photoshop或Word中创建一个空白A4文档,导出为PDF保存到resources目录。代码中通过ClassLoader读取:
private static final String EMPTY_PDF = "/templates/empty_a4.pdf";为什么要用空白PDF而不是直接创建?因为iText直接创建PDF时,默认边距和打印机会有兼容性问题。使用预制的空白PDF能确保打印效果一致。
3. 核心实现步骤详解
3.1 PDF转图片处理
滴滴的发票和行程单都是PDF格式,但直接合并PDF会遇到页面尺寸不一致的问题。我的方案是先转为图片再处理:
BufferedImage image = PDFToImageSample.pdfToBufferedImage(pdfPath, 300);这里DPI设置为300是为了保证打印清晰度。实际测试发现,低于200DPI打印会模糊,高于400DPI又会导致文件过大。
3.2 图片优化处理
行程单顶部有滴滴的广告banner,报销时不需要这部分。我们可以用Java的图像处理功能裁剪掉:
travelBI = travelBI.getSubimage(0, TRAVEL_Y, travelBI.getWidth(), travelBI.getHeight() - TRAVEL_H);TRAVEL_Y和TRAVEL_H是需要根据具体PDF调整的参数。建议先用PDF阅读器测量banner的像素高度,再在代码中设置。
3.3 智能合并到A4页面
这是最关键的步骤——将两张图片合理地排列到一个A4页面中:
contractSealImg.setAbsolutePosition(ABSOLUTE_X, (width + height) / 2);我经过多次试验,发现发票放在上半部分,行程单放下半部分最合理。ABSOLUTE_X控制水平居中位置,第二个参数控制垂直位置。如果发现位置偏移,可以微调这些参数。
4. 高级功能扩展
4.1 灰度打印模式
很多公司要求报销材料用黑白打印,我们可以添加灰度处理:
ColorConvertOp op = new ColorConvertOp( ColorSpace.getInstance(ColorSpace.CS_GRAY), null); op.filter(src, dest);这个处理会显著减小文件大小,实测从原来的1MB左右降到300KB左右。
4.2 批量处理优化
如果需要处理大量发票,可以改造为批量模式:
ExecutorService executor = Executors.newFixedThreadPool(4); List<Future<File>> futures = new ArrayList<>(); for (File pdf : pdfFiles) { futures.add(executor.submit(new MergeTask(pdf))); }我建议使用线程池控制并发数,避免同时处理太多文件导致内存溢出。每个线程处理完后,可以把结果保存到指定目录。
4.3 其他PDF合并场景
这套方案不只适用于滴滴发票,稍作修改就能处理其他场景:
- 合并扫描的合同附件
- 将多页报表压缩到一页
- 生成带水印的PDF
关键是要理解iText的PdfStamper和PdfContentByte的工作原理。掌握了这些核心类,各种PDF操作都不在话下。
5. 实际应用中的注意事项
5.1 字体嵌入问题
如果PDF中有特殊字体,需要确保字体嵌入,否则转换图片时会出现乱码:
BaseFont baseFont = BaseFont.createFont( "STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);中文字体处理是个大坑,建议使用系统自带字体或确保字体文件在classpath中。
5.2 内存管理
处理大型PDF时容易内存溢出,要注意及时关闭流:
finally { if (stamper != null) stamper.close(); if (reader != null) reader.close(); }我建议为处理程序设置内存上限,比如通过JVM参数-Xmx512m限制最大内存。
5.3 异常处理
网络下载的PDF可能损坏,要添加健壮的异常处理:
try { // PDF处理代码 } catch (BadPdfFormatException e) { logger.error("PDF格式错误", e); } catch (IOException e) { logger.error("IO异常", e); }特别是处理用户上传的文件时,各种奇怪的情况都可能出现。好的异常处理能让程序更稳定。
6. 性能优化实践
6.1 缓存优化
频繁创建PdfReader实例很耗资源,可以缓存常用PDF:
private static final Map<String, PdfReader> pdfCache = new ConcurrentHashMap<>();但要注意及时清理缓存,避免内存泄漏。可以设置LRU策略自动淘汰不常用的PDF。
6.2 图片压缩技巧
在保证清晰度的前提下减小图片大小:
JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null); jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); jpegParams.setCompressionQuality(0.8f);0.8的压缩质量在文件大小和清晰度间取得了很好的平衡。可以做成可配置参数,让用户自己调整。
6.3 异步处理方案
对于耗时操作,建议采用异步处理:
@Async public Future<File> processPdfAsync(File input) { // 处理逻辑 return new AsyncResult<>(output); }Spring的@Async注解让实现异步变得简单。前端可以轮询或使用WebSocket获取处理进度。
7. 完整代码结构解析
核心方法mergeDiDiInvoiceAndTravelToOnePDF的主要逻辑:
- 参数校验:检查灰度类型是否有效
- PDF转图片:使用300DPI保证质量
- 图片处理:裁剪、灰度转换
- 合并到PDF:精确定位两张图片
- 资源清理:关闭所有IO流
我特别建议把常量参数如DEFAULT_DPI、ABSOLUTE_X等提取为配置项,这样不同场景下调整更方便。比如:
@Value("${pdf.dpi:300}") private int defaultDpi;这样后续要修改DPI,直接改配置文件就行,不需要重新编译代码。
