ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java PDF处理实战:OpenPDF中文支持、表单填充与性能优化指南

Java PDF处理实战:OpenPDF中文支持、表单填充与性能优化指南

1. 从PDF处理痛点说起:为什么选择OpenPDF?

如果你在Java项目中处理过PDF,大概率经历过这样的场景:客户发来一份合同,需要你自动填充几个字段然后生成新文件;或者,你需要从一堆报告里提取特定表格的数据;又或者,领导要求给所有对外PDF加上公司水印和页脚。这时候,你打开搜索引擎,输入“Java PDF库”,结果可能会让你眼花缭乱——iText、Apache PDFBox、PDF Clown…… 每个都号称功能强大,但文档要么是收费的,要么是晦涩难懂的,要么就是性能或内存管理上有些“坑”。

我最初接触PDF处理时,用的是当时最流行的库。功能确实强大,但商业许可费用不菲,对于很多预算有限的中小项目或个人开发者来说,是个不小的门槛。后来转向了Apache PDFBox,它是一个纯Java的开源库,免费且活跃,但在处理一些复杂的页面布局、字体嵌入,特别是涉及中文等非拉丁字符集时,配置起来相当繁琐,稍有不慎就会遇到乱码或者布局错乱的问题。更头疼的是,对于修改现有PDF(如表单填充、内容替换)这类“编辑”操作,PDFBox的API设计得比较底层,需要开发者对PDF的内部结构有较深的理解,上手成本不低。

正是在这种背景下,我发现了OpenPDF。它最初是基于iText 2.1.7版本的一个分支,而iText 2.x系列采用的是LGPL/MPL开源协议,这意味着OpenPDF继承了其核心功能,并保持了开源和免费(基于LGPL v2.1)。对于大多数常见的PDF生成、读取和轻度编辑任务,OpenPDF提供了一个更轻量、更易上手的选择。它的API设计在很大程度上保留了iText 2.x的风格,对于有过相关经验的开发者来说几乎可以无缝切换,而对于新手,其学习曲线也相对平缓。更重要的是,它在处理中文等亚洲字体方面,有着经过验证的、相对可靠的解决方案。

所以,这篇内容不是一份冰冷的API文档翻译,而是结合我多次在真实项目(包括报表生成、合同处理、文档转换)中使用OpenPDF的经验,为你梳理的一份实战指南。我会带你从零开始搭建环境,通过几个最典型的样例代码,手把手教你如何完成创建、编辑、读取PDF等核心操作,并分享那些官方文档里不会写的“踩坑”心得和性能调优技巧。

2. 环境准备与项目搭建:迈出第一步

在开始写代码之前,我们需要先把舞台搭好。OpenPDF的依赖管理和项目结构很简单,但有几个细节不注意,后面可能会浪费你不少调试时间。

2.1 依赖引入:Maven与Gradle配置

OpenPDF的主要构件托管在Maven中央仓库,引入非常方便。这里以Maven为例,Gradle的配置逻辑是类似的。

在你的项目pom.xml文件的<dependencies>部分,添加以下依赖:

<dependency> <groupId>com.github.librepdf</groupId> <artifactId>openpdf</artifactId> <version>1.3.30</version> <!-- 请检查并使用最新稳定版本 --> </dependency>

注意:版本号请务必前往 OpenPDF的GitHub发布页面 或 Maven中央仓库查看最新稳定版。开源项目迭代较快,使用新版本通常能获得Bug修复和性能改进。

为什么是这个依赖项?早期有些教程可能会引用com.lowagie的groupId,那是iText 2.x时代的遗留物。OpenPDF项目迁移后,统一的groupId是com.github.librepdf。使用错误的groupId会导致无法解析依赖。

添加依赖后,Maven会自动下载OpenPDF的jar包及其传递依赖(例如,用于XML解析的库)。通常,OpenPDF本身依赖很少,这使它成为一个相当轻量级的库。

2.2 字体准备:中文支持的基石

处理中文PDF,字体是第一个,也是最重要的坎。PDF文件内部并不“携带”完整的字体文件,而是嵌入所使用的字体的一个子集(或全部)。如果生成PDF时没有正确嵌入中文字体,那么在其他没有安装该字体的设备上打开,轻则显示为乱码,重则直接无法显示文本。

OpenPDF处理字体的核心类是com.lowagie.text.Fontcom.lowagie.text.pdf.BaseFont。我们需要使用BaseFont来加载一个支持中文的字体文件(通常是.ttf.ttc格式)。

操作步骤:

  1. 获取字体文件:你可以使用系统自带的字体(如Windows的simsun.ttc宋体),但请注意版权问题。对于商业项目,强烈建议使用开源字体,例如“思源”系列(Source Han Sans/Source Han Serif,即“思源黑体/宋体”)、”方正“系列的开源版本或“文泉驿”字体。这里我们以开源免费的“思源黑体”为例。
  2. 将字体文件放入项目:通常的做法是在项目的src/main/resources/fonts/目录下创建一个文件夹,将你的.ttf文件放进去。这样,它会被打包到最终的jar或war文件中,便于通过类路径(classpath)访问。
  3. 加载字体:在代码中,使用BaseFont.createFont方法来加载字体。关键是指定正确的路径和编码。
// 假设字体文件位于 resources/fonts/SourceHanSansSC-Regular.ttf BaseFont baseFont = BaseFont.createFont( "fonts/SourceHanSansSC-Regular.ttf", // 相对于classpath的路径 BaseFont.IDENTITY_H, // 编码,对于中日韩等双字节字符使用IDENTITY_H BaseFont.EMBEDDED // 嵌入选项,必须嵌入 );

参数详解与避坑指南:

  • 路径"fonts/SourceHanSansSC-Regular.ttf"。这里使用的是相对于classpath的路径。如果你的文件放在resources根目录下,就是"SourceHanSansSC-Regular.ttf"。绝对路径也可以,但会降低项目的可移植性。
  • 编码BaseFont.IDENTITY_H:这是最关键的一步。IDENTITY_H表示使用 Unicode 水平书写编码。对于中文、日文、韩文等字符,必须使用此编码或IDENTITY_V(垂直书写),才能确保字符被正确映射和嵌入。使用默认的BaseFont.WINANSIBaseFont.CP1252等单字节编码必然导致中文显示为空白或乱码。
  • 嵌入选项BaseFont.EMBEDDED:这个参数告诉OpenPDF,将字体文件(或子集)嵌入到生成的PDF中。这样,即使用户电脑上没有这个字体,PDF也能正确显示。如果你确定所有用户环境都有该字体,可以使用BaseFont.NOT_EMBEDDED,但这在跨平台交付时风险极高,不推荐。

常见问题:如果运行时抛出IOException或提示找不到字体文件,请检查:

  1. 文件路径是否正确,大小写是否敏感(Linux环境下)。
  2. 字体文件是否真的被打包到了你的应用jar/war中。可以解压生成的jar包查看。
  3. 字体文件本身是否损坏。

准备好环境和字体,我们就有了对付PDF的“武器”。接下来,让我们从最简单的任务开始:创建一份全新的PDF文档。

3. 核心实战一:创建一份全新的PDF文档

创建新PDF是OpenPDF最基础的功能。我们将创建一个包含标题、段落、列表和简单表格的A4尺寸文档,并解决中文排版问题。

3.1 文档初始化与基本设置

一切始于com.lowagie.text.Document类。它代表一个PDF文档。

import com.lowagie.text.*; import com.lowagie.text.pdf.PdfWriter; import java.io.FileOutputStream; public class CreatePdfDemo { public static void main(String[] args) throws Exception { // 1. 创建Document实例,指定页面大小和边距 // PageSize.A4 是常用尺寸,你也可以用 new Rectangle(width, height) 自定义 // 参数依次是:左、右、上、下边距(单位:像素) Document document = new Document(PageSize.A4, 50, 50, 50, 50); // 2. 获取PdfWriter实例,将document写入文件 // PdfWriter.getInstance(文档对象, 输出流) PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream("我的第一个OpenPDF文档.pdf")); // 3. 打开文档,开始写入内容 document.open(); // --- 在这里添加内容 (见3.2) --- // 4. 关闭文档。这是必须的,它会完成写入并释放资源。 document.close(); System.out.println("PDF文档创建成功!"); } }

关键点解析:

  • PdfWriter.getInstance(document, outputStream):这个静态方法建立了Document和底层PDF输出流之间的桥梁。PdfWriter负责将我们添加的高层元素(如段落、表格)转换为PDF的底层指令。
  • document.open()document.close():必须成对出现。open()之后才能添加内容,close()会进行最终的整理和写入,并关闭PdfWriter和输出流。忘记close()可能导致生成的PDF文件损坏或内容不全。

3.2 添加文本内容:段落、字体与中文

现在,我们在document.open()document.close()之间添加内容。首先解决中文段落。

// ... 接上面的代码,在 document.open() 之后 ... // 加载中文字体 (使用上一节准备好的方法) BaseFont baseFont = BaseFont.createFont("fonts/SourceHanSansSC-Regular.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); // 创建字体对象,设置大小和样式 Font titleFont = new Font(baseFont, 18, Font.BOLD); // 18号,粗体 Font normalFont = new Font(baseFont, 12, Font.NORMAL); // 12号,正常 Font highlightFont = new Font(baseFont, 12, Font.ITALIC | Font.UNDERLINE); // 12号,斜体加下划线 // 1. 添加标题 Paragraph title = new Paragraph("OpenPDF 实战教程:创建PDF文档", titleFont); title.setAlignment(Element.ALIGN_CENTER); // 居中对齐 title.setSpacingAfter(20f); // 设置段后间距 document.add(title); // 2. 添加普通段落 Paragraph p1 = new Paragraph("这是一段使用OpenPDF生成的中文内容。它演示了如何设置字体、对齐方式和间距。", normalFont); p1.setFirstLineIndent(24); // 首行缩进24个单位(约两个汉字) p1.setSpacingAfter(15f); document.add(p1); // 3. 添加带特殊样式的段落 Paragraph p2 = new Paragraph(); // 可以组合不同样式的文本到同一个段落 p2.add(new Chunk("注意:", highlightFont)); p2.add(new Chunk(" 如果遇到中文显示为方块或空白,请务必检查BaseFont的编码是否为 `BaseFont.IDENTITY_H`。", normalFont)); document.add(p2); // 4. 添加一个无序列表 List list = new List(List.UNORDERED); // 无序列表 list.setListSymbol("\u2022"); // 设置列表符号为圆点,也可以使用图片 list.setIndentationLeft(20); // 列表整体缩进 list.add(new ListItem("支持创建新PDF文档", normalFont)); list.add(new ListItem("支持编辑现有PDF(如表单填充)", normalFont)); list.add(new ListItem("支持从PDF提取文本和元数据", normalFont)); list.add(new ListItem("开源且免费(LGPL协议)", normalFont)); document.add(list);

经验之谈:

  • Chunk是文本的最小单元,可以拥有独立的字体样式。PhraseChunk的序列,而ParagraphPhrase加上段落格式(缩进、对齐、间距)。通常,直接使用Paragraph并设置字体就够了,需要混合样式时才用Chunk组合。
  • setSpacingBeforesetSpacingAfter用于控制段落间距,这在排版时非常有用。
  • 列表的符号可以通过setListSymbol自定义,甚至可以使用ChunkImage对象来实现更复杂的符号。

3.3 添加简单表格

表格是报表类PDF的常客。OpenPDF的com.lowagie.text.pdf.PdfPTable类功能强大,但也有一些“脾气”。

// ... 接上面代码,继续添加 ... // 5. 创建一个3列4行的表格 PdfPTable table = new PdfPTable(3); // 参数指定列数 table.setWidthPercentage(100); // 表格宽度占页面的100% table.setSpacingBefore(20f); // 表格前间距 table.setSpacingAfter(20f); // 设置表头(可选,但通常需要) Font headerFont = new Font(baseFont, 13, Font.BOLD); table.addCell(new PdfPCell(new Phrase("产品名称", headerFont))); table.addCell(new PdfPCell(new Phrase("规格", headerFont))); table.addCell(new PdfPCell(new Phrase("单价(元)", headerFont))); // 添加表格数据 Font cellFont = new Font(baseFont, 11, Font.NORMAL); table.addCell(new PdfPCell(new Phrase("Java编程思想", cellFont))); table.addCell(new PdfPCell(new Phrase("第4版", cellFont))); table.addCell(new PdfPCell(new Phrase("108.50", cellFont))); table.addCell(new PdfPCell(new Phrase("OpenPDF官方指南", cellFont))); table.addCell(new PdfPCell(new Phrase("在线文档", cellFont))); table.addCell(new PdfPCell(new Phrase("0.00", cellFont))); // 设置单元格样式(如背景色、边框、内边距) PdfPCell cell = new PdfPCell(new Phrase("总计", new Font(baseFont, 12, Font.BOLD))); cell.setColspan(2); // 合并前两列 cell.setHorizontalAlignment(Element.ALIGN_RIGHT); cell.setPadding(5); table.addCell(cell); table.addCell(new PdfPCell(new Phrase("108.50", new Font(baseFont, 12, Font.BOLD)))); document.add(table);

表格使用中的坑:

  1. 列宽问题:创建PdfPTable时只指定了列数,列宽默认是均分的。你可以通过table.setTotalWidth()table.setWidths(float[] relativeWidths)来精确控制每列宽度。如果不设置,在内容长度差异大时,表格可能很难看。
  2. 单元格内容过多:如果一个单元格的内容太长,OpenPDF默认会撑高该行。如果不想换行,可以设置cell.setNoWrap(true),但内容可能会溢出。更常见的做法是调整字体大小或列宽。
  3. 跨页表格:当表格很长,一页放不下时,OpenPDF会自动将其拆分到多页。表头默认不会在后续页重复。如果需要重复表头,需要在添加完所有表头单元格后,调用table.setHeaderRows(1),参数1表示第一行是表头。
  4. 性能:向一个超大型表格(成千上万行)连续添加单元格时,可能会遇到内存和性能问题。对于海量数据,考虑分页生成或多个表格。

运行上面的完整代码,你将得到一个包含中文标题、段落、列表和表格的规范PDF文件。这只是开始,现实中我们更多需要与已有的PDF文件打交道。

4. 核心实战二:读取与解析现有PDF内容

很多时候,我们需要从PDF中提取信息,比如读取发票号码、解析报告中的特定数据。OpenPDF提供了com.lowagie.text.pdf.PdfReader类来读取PDF。

4.1 读取PDF元信息与页数

PdfReader不仅能读取内容,还能获取文档的“元数据”。

import com.lowagie.text.pdf.PdfReader; import java.io.IOException; public class ReadPdfMetaDemo { public static void main(String[] args) throws IOException { String filePath = "待读取的文档.pdf"; // 创建PdfReader实例。务必在finally块中关闭或使用try-with-resources。 try (PdfReader reader = new PdfReader(filePath)) { // 1. 获取页数 int numberOfPages = reader.getNumberOfPages(); System.out.println("文档总页数: " + numberOfPages); // 2. 获取文档信息 (元数据) com.lowagie.text.pdf.PdfDictionary info = reader.getInfo(); String title = info.getAsString(PdfName.TITLE) != null ? info.getAsString(PdfName.TITLE).toString() : "无"; String author = info.getAsString(PdfName.AUTHOR) != null ? info.getAsString(PdfName.AUTHOR).toString() : "无"; String subject = info.getAsString(PdfName.SUBJECT) != null ? info.getAsString(PdfName.SUBJECT).toString() : "无"; System.out.println("标题: " + title); System.out.println("作者: " + author); System.out.println("主题: " + subject); // 3. 获取其他属性 System.out.println("文件是否加密: " + reader.isEncrypted()); System.out.println("PDF版本: " + reader.getPdfVersion()); } catch (IOException e) { System.err.println("读取PDF文件失败: " + e.getMessage()); e.printStackTrace(); } } }

注意PdfReader构造函数可能会因为文件被占用、路径错误或PDF文件损坏而抛出IOException。使用try-with-resources语法(Java 7+)可以确保资源被正确关闭。

4.2 提取页面文本内容

提取文本是更常见的需求。OpenPDF提供了com.lowagie.text.pdf.parser.PdfTextExtractor工具类。

import com.lowagie.text.pdf.parser.PdfTextExtractor; public class ExtractTextDemo { public static void main(String[] args) throws IOException { String filePath = "待提取的文档.pdf"; try (PdfReader reader = new PdfReader(filePath)) { int totalPages = reader.getNumberOfPages(); // 策略一:提取所有页面的文本 StringBuilder allText = new StringBuilder(); for (int i = 1; i <= totalPages; i++) { // 页码从1开始 String textFromPage = PdfTextExtractor.getTextFromPage(reader, i); allText.append("--- 第 ").append(i).append(" 页 ---\n"); allText.append(textFromPage).append("\n\n"); } System.out.println(allText.toString()); // 策略二:只提取特定页面(例如第2页) // String page2Text = PdfTextExtractor.getTextFromPage(reader, 2); // System.out.println(page2Text); } catch (IOException e) { e.printStackTrace(); } } }

文本提取的局限性:

  • 布局信息丢失PdfTextExtractor返回的是纯文本,原有的表格、分栏、图片位置等布局信息完全丢失。文本顺序大体遵循阅读顺序,但对于复杂排版,顺序可能错乱。
  • 编码问题:如果PDF中的文本使用了非标准编码或自定义字体,且未正确嵌入,提取出的文本可能是乱码。OpenPDF会尽力解析,但并非万能。
  • 扫描件/图片PDF:对于由扫描图片构成的PDF,PdfTextExtractor无法提取任何文字。这类需求属于OCR(光学字符识别)范畴,需要集成Tesseract等OCR库。

4.3 进阶:使用LocationTextExtractionStrategy获取文本位置

如果你需要知道文本在页面上的坐标(例如,想高亮某个关键词),就需要更底层的解析策略。

import com.lowagie.text.pdf.parser.*; import java.util.List; public class ExtractTextWithLocationDemo { public static void main(String[] args) throws IOException { try (PdfReader reader = new PdfReader("待解析的文档.pdf")) { // 创建一个策略,用于接收解析出的文本及其位置 LocationTextExtractionStrategy strategy = new LocationTextExtractionStrategy(); // 使用PdfReaderContentParser来解析指定页面,并应用我们的策略 PdfReaderContentParser parser = new PdfReaderContentParser(reader); parser.processContent(1, strategy); // 解析第1页 // 获取解析结果 String extractedText = strategy.getResultantText(); System.out.println("提取的文本:\n" + extractedText); // 获取所有文本块及其位置信息(高级用法) // 注意:LocationTextExtractionStrategy的内部实现可能不直接暴露位置列表。 // 如果需要精确位置,可能需要自定义一个实现TextExtractionStrategy接口的类。 // 这里演示一个简化思路: List<TextRenderInfo> textInfos = strategy.getLocationalResult(); // 注意:此方法名可能不准确,实际API请查阅文档 // 遍历textInfos,可以通过getBaseline()等方法获取坐标 // 由于OpenPDF API在此处略有变化,具体实现需参考其Javadoc或源码。 } catch (IOException e) { e.printStackTrace(); } } }

提示:对于需要精确定位文本的需求,OpenPDF的API不如iText 7或PDFBox直观。如果这是核心需求,可能需要评估其他库,或者深入研究OpenPDF的com.lowagie.text.pdf.parser包,自定义RenderListener

读取和解析是“只读”操作。接下来,我们看看如何“写入”或“修改”一个已有的PDF,这是表单填充、添加水印等场景的核心。

5. 核心实战三:编辑现有PDF(表单填充与水印)

OpenPDF编辑PDF的核心是使用PdfStamper。它允许你在一个已有的PDF(由PdfReader读取)上叠加新的内容,而不会重写整个文件结构,效率较高。

5.1 填充PDF表单(AcroForm)

许多PDF文件是交互式表单,包含文本框、复选框、单选按钮等字段。OpenPDF可以读取和填充这些字段。

前提:你的PDF必须是包含表单(AcroForm)的。可以用Adobe Acrobat或Foxit等工具查看是否有表单字段。

import com.lowagie.text.pdf.*; public class FillPdfFormDemo { public static void main(String[] args) throws Exception { String srcPdf = "带表单的原始合同.pdf"; String destPdf = "填充后的合同.pdf"; try (PdfReader reader = new PdfReader(srcPdf); FileOutputStream out = new FileOutputStream(destPdf)) { // 1. 创建PdfStamper // 第二个参数是输出流,第三个参数(可选)指定PDF版本,'\0'表示保持原版本 PdfStamper stamper = new PdfStamper(reader, out, '\0', true); // 最后一个true表示保留压缩 // 2. 获取表单字段 AcroFields form = stamper.getAcroFields(); // 3. 列出所有表单字段名(调试用) System.out.println("所有表单字段:"); for (String fieldName : form.getFields().keySet()) { System.out.println(" - " + fieldName); } // 4. 填充字段值 // 假设表单中有名为"clientName", "contractDate", "signature"的字段 form.setField("clientName", "张三科技有限公司"); form.setField("contractDate", "2023-10-27"); form.setField("signature", "已签署"); // 对于图片签名,需要更复杂的处理 // 5. 如果字段是复选框/单选按钮,设置值通常是"On"表示选中,""或"Off"表示未选中 // 具体值需要查看表单字段的属性 // form.setField("agreeTerms", "On"); // 6. 处理中文字体(如果表单字段原本不支持中文显示) // 如果填充后中文显示为乱码,可能需要为字段设置中文字体 BaseFont baseFont = BaseFont.createFont("fonts/SourceHanSansSC-Regular.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); form.setFieldProperty("clientName", "textfont", baseFont, null); // 为多个字段统一设置字体 for (String fieldName : form.getFields().keySet()) { form.setFieldProperty(fieldName, "textfont", baseFont, null); } // 7. 如果希望表单在查看时就是扁平化的(不可再编辑),可以设置 // stamper.setFormFlattening(true); // 8. 关闭stamper,完成写入 stamper.close(); System.out.println("表单填充完成,文件保存至: " + destPdf); } catch (Exception e) { System.err.println("处理PDF表单失败: " + e.getMessage()); e.printStackTrace(); } } }

表单填充的深坑与技巧:

  1. 字段名匹配:字段名(field name)是大小写敏感的,且可能包含空格或特殊字符。最好先用form.getFields().keySet()打印出来确认。
  2. 字体嵌入(再次强调):如果填充的中文不显示,99%的原因是字段没有使用支持中文的字体。通过setFieldProperty设置textfont属性至关重要。BaseFont必须使用IDENTITY_H编码和EMBEDDED选项。
  3. 扁平化(Flattening)stamper.setFormFlattening(true)会将表单字段转换为普通的PDF文本和图形,此后表单将无法再编辑。这在最终签署、归档时非常有用。注意:扁平化后,之前设置的字段字体属性可能不再起作用,因为字段已经不存在了。
  4. 性能:对于字段非常多的大型表单,逐个设置字体属性可能影响性能。可以尝试批量设置。

5.2 为PDF添加水印

水印可以是文本(如“草稿”、“机密”),也可以是图片。这里演示添加一个倾斜的、半透明的文本水印到每一页。

import com.lowagie.text.*; import com.lowagie.text.pdf.*; public class AddWatermarkDemo { public static void main(String[] args) throws Exception { String srcPdf = "原始文档.pdf"; String destPdf = "带水印的文档.pdf"; try (PdfReader reader = new PdfReader(srcPdf); FileOutputStream out = new FileOutputStream(destPdf); PdfStamper stamper = new PdfStamper(reader, out)) { // 加载中文字体用于水印文字 BaseFont baseFont = BaseFont.createFont("fonts/SourceHanSansSC-Regular.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); Font watermarkFont = new Font(baseFont, 60, Font.BOLD, new Color(200, 200, 200, 128)); // 灰色,半透明 // 获取总页数 int totalPages = reader.getNumberOfPages(); // 获取PDF页面尺寸 Rectangle pageSize = reader.getPageSize(1); float width = pageSize.getWidth(); float height = pageSize.getHeight(); // 为每一页添加水印 for (int i = 1; i <= totalPages; i++) { // 获取当前页的覆盖层(Content Byte) PdfContentByte over = stamper.getOverContent(i); // 在现有内容之上绘制 // 如果要放在内容之下,用 stamper.getUnderContent(i) over.saveState(); // 保存当前图形状态 over.setGState(new PdfGState().setFillOpacity(0.3f)); // 设置整体透明度 // 开始文本绘制 over.beginText(); over.setFontAndSize(baseFont, 60); over.setColorFill(new Color(150, 150, 150)); // 水印颜色 // 计算水印位置:居中并旋转45度 over.showTextAligned(Element.ALIGN_CENTER, "内部传阅 严禁外泄", width / 2, height / 2, 45); // x, y, rotation // 你也可以添加多个水印或使用循环平铺 // for (float x = 0; x < width; x += 200) { // for (float y = 0; y < height; y += 150) { // over.showTextAligned(Element.ALIGN_CENTER, "CONFIDENTIAL", // x, y, 45); // } // } over.endText(); over.restoreState(); // 恢复图形状态 } stamper.close(); System.out.println("水印添加完成,文件保存至: " + destPdf); } catch (Exception e) { e.printStackTrace(); } } }

水印制作要点:

  • getOverContent(i)vsgetUnderContent(i):决定水印在原有内容的上面还是下面。通常“草稿”水印用OverContent盖在上面,“信纸背景”水印用UnderContent垫在下面。
  • 透明度:通过PdfGState().setFillOpacity()设置填充透明度,值在0.0(完全透明)到1.0(完全不透明)之间。通过new Color(R, G, B, A)也可以设置带透明通道的颜色。
  • 位置与旋转showTextAligned方法非常方便,可以指定对齐方式、坐标和旋转角度。旋转中心是文本的定位点。
  • 性能:在每一页上绘制复杂水印(如图片、平铺文字)可能会增加文件大小和处理时间。

通过PdfStamper,我们实现了对PDF的“增量”修改。但有些操作,比如彻底删除或重新排序页面,PdfStamper就力不从心了,这时需要用到更强大的PdfCopyPdfSmartCopy

6. 高级操作与性能优化

当任务超出简单的创建、读取和叠加时,我们就需要一些高级工具和技巧。

6.1 合并多个PDF文档

合并PDF是一个常见需求。OpenPDF提供了PdfCopy类,但它主要复制页面内容,可能不会智能地处理重复的资源(如图片、字体),导致合并后的文件膨胀。PdfSmartCopyPdfCopy的增强版,会尝试重用资源,更适合合并多个包含相同资源的文档。

import com.lowagie.text.Document; import com.lowagie.text.pdf.*; public class MergePdfsDemo { public static void main(String[] args) throws Exception { String[] sourcePdfs = {"文档1.pdf", "文档2.pdf", "文档3.pdf"}; String destPdf = "合并后的文档.pdf"; Document mergedDoc = new Document(); try (FileOutputStream out = new FileOutputStream(destPdf)) { // 使用PdfSmartCopy来优化资源处理 PdfSmartCopy copy = new PdfSmartCopy(mergedDoc, out); mergedDoc.open(); for (String srcFile : sourcePdfs) { try (PdfReader reader = new PdfReader(srcFile)) { int pages = reader.getNumberOfPages(); for (int i = 1; i <= pages; i++) { // 将当前文档的每一页添加到合并文档中 copy.addPage(copy.getImportedPage(reader, i)); } // 可选:在文档间添加一个空白页或分页符 // mergedDoc.newPage(); } catch (IOException e) { System.err.println("读取文件失败: " + srcFile); e.printStackTrace(); } } mergedDoc.close(); // 会自动关闭copy System.out.println("PDF合并完成!"); } catch (Exception e) { e.printStackTrace(); } } }

合并时的注意事项:

  • 页面尺寸:如果被合并的PDF页面尺寸不一致,合并后的文档页面尺寸将以第一个添加的页面为准。后续不同尺寸的页面可能会被缩放或裁剪,导致内容显示不全。需要在合并前统一页面尺寸,或使用更复杂的逻辑处理。
  • 书签(目录)PdfSmartCopy不会自动合并源文档的书签。如果需要保留书签,需要手动处理PdfReader.getOutlines(),这是一个相对高级的操作。
  • 表单字段:合并后,表单字段的名称可能会冲突,导致行为异常。合并带表单的PDF需要格外小心。

6.2 内存管理与性能调优

处理大型或大量PDF时,内存和性能是关键。

  1. 及时关闭资源PdfReader,PdfStamper,Document都持有文件流或内存资源。务必使用try-with-resources或在finally块中确保它们被关闭。
  2. 使用PdfReader的局部加载模式
    // 对于非常大的PDF,可以只将部分页面加载到内存 PdfReader reader = new PdfReader("超大文件.pdf"); reader.selectPages("1-5, 10-15"); // 只处理第1-5页和10-15页 // 后续操作只针对选中的页面 reader.close();
  3. 避免在循环中重复创建BaseFont:加载字体文件是I/O密集型操作。应该在程序初始化时加载一次,然后缓存起来重复使用。
    public class FontCache { private static BaseFont chineseFont; static { try { chineseFont = BaseFont.createFont("fonts/SourceHanSansSC-Regular.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } catch (Exception e) { throw new RuntimeException("加载字体失败", e); } } public static BaseFont getChineseFont() { return chineseFont; } }
  4. 对于批量生成,考虑复用DocumentPdfWriter:如果需要生成成千上万个结构相似的小PDF,频繁创建和销毁Document对象会有开销。可以研究在内存中操作,或者使用更底层的API。但对于大多数场景,为每个文件创建新对象是清晰且安全的做法。
  5. 监控内存:在处理过程中,可以使用Runtime.getRuntime().totalMemory()freeMemory()来监控内存使用情况,防止OutOfMemoryError。

6.3 常见错误排查与调试

  • IOException: PDF header signature not found:文件不是PDF,或者已损坏。
  • ClassCastExceptionIllegalArgumentException:通常是因为PDF内部结构不符合预期,或者你尝试用错误的方法操作一个元素(例如,对非表单字段设置字段属性)。仔细检查你的代码逻辑和PDF文件本身。
  • 中文显示为方框:请百分百确认:
    1. BaseFont.createFont的第二个参数是BaseFont.IDENTITY_H
    2. 第三个参数是BaseFont.EMBEDDED
    3. 字体文件路径正确且可读。
    4. PdfStamper中填充表单时,通过setFieldProperty设置了字段字体。
  • 生成的PDF文件损坏无法打开:大概率是document.close()stamper.close()没有被执行。确保所有异常路径下资源也能被关闭(使用try-with-resources)。
  • 内容重叠或位置错误:检查你添加元素时使用的坐标系统。OpenPDF默认使用用户空间单位(通常1单位=1像素),原点在页面左下角。使用PdfContentByte直接绘图时,尤其要注意坐标计算。

OpenPDF是一个在功能、易用性和许可协议之间取得了很好平衡的库。对于大多数常见的PDF处理任务,它都能胜任。它的API设计带有一定的历史痕迹,不如一些现代库那么优雅,但正因为其稳定和轻量,在许多生产系统中依然扮演着关键角色。掌握它,意味着你拥有了在Java世界里处理PDF文档的一把可靠钥匙。

返回列表