1. 为什么我们需要合并滴滴发票和行程单每次出差或加班打车后财务报销总是个麻烦事。滴滴出行会生成两个PDF文件——电子发票和行程单分开打印不仅浪费纸张还容易丢失。更糟的是很多PDF合并工具要么收费要么有水印要么操作复杂。作为经常需要报销的Java开发者我决定用iText这个强大的PDF处理库自己动手解决这个问题。iText是Java生态中最成熟的PDF处理库之一它不仅能生成PDF还能对现有PDF进行各种操作。实测下来用iText处理滴滴的发票和行程单合并效果比市面上大多数工具都好。最关键的是整个过程完全自动化再也不用手动调整页面大小和位置了。2. iText环境准备与基础配置2.1 引入iText依赖首先需要在项目中加入iText依赖。如果你使用Maven在pom.xml中添加dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency dependency groupIdcom.itextpdf.tool/groupId artifactIdxmlworker/artifactId version5.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); ListFutureFile 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实例很耗资源可以缓存常用PDFprivate static final MapString, 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 FutureFile 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直接改配置文件就行不需要重新编译代码。