【PDFBox】-初识
我们在实际开发中经常碰到需要对PDF进行写入(如:签名、水印等)、读取(如:读取发票信息等),甚至某些情况下要对PDF进行结构化的解析。要解析某种文件,我们当然要先了解下这个文件的基本格式都包含哪些元素。千万别直接去学解析的工具和框架,你会看的一头雾水,就像盲人摸象一样,不了解就会导致结果云里雾里。那首先,我们先来了解下,PDF的文档格式。
PDF格式介绍
PDF(Portable Document Format)文件是一种广泛使用的文档格式,用于表示文档的固定布局。PDF 文件的结构呢,其实细节很多,我们先了解主要内容,基本能覆盖80%以上的场景,主要包括以下几个部分:
1. 文件头(File Header)
- 标识符:以
%PDF-
开头,后面跟着版本号,例如%PDF-1.7
。 - 注释:文件头可能包含一些注释,通常用于防止某些旧的打印机或软件误读文件。
2. 主体(Body)
- 对象:PDF 文件中的所有内容都以对象的形式存在。对象可以是直接对象(如数字、字符串、布尔值等)或间接对象(如字典、数组、流等)。
- 间接对象:每个间接对象都有一个唯一的对象编号和一个版本号。例如:
这里,1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
1 0 obj
表示这是一个编号为 1、版本为 0 的对象,<< /Type /Catalog /Pages 2 0 R >>
是一个字典对象,endobj
标志对象结束。
3. 交叉引用表(Cross-Reference Table)
- XRef 表:记录了每个间接对象在文件中的位置,以便快速查找。XRef 表的格式如下:
xref 0 5 0000000000 65535 f 0000000010 00000 n 0000000078 00000 n 0000000123 00000 n 0000000234 00000 n
- 第一行
xref
表示 XRef 表的开始。 - 第二行
0 5
表示从对象编号 0 开始,共有 5 个条目。 - 每个条目包含 20 个字符,前 10 个字符是对象在文件中的偏移量,中间 5 个字符是对象的版本号,最后一个字符
f
或n
表示对象的状态(f
表示自由对象,n
表示已使用对象)。
- 第一行
4. 文件尾(Trailer)
- 尾部字典:包含了一些重要的信息,如根对象的引用、加密信息等。格式如下:
trailer << /Size 5 /Root 1 0 R /Info 3 0 R >>
/Size
表示文件中对象的总数。/Root
指向根对象,通常是目录对象。/Info
指向文档的信息字典。
5. 启动代码(StartXRef)
- 启动代码:指出了 XRef 表在文件中的起始位置。格式如下:
startxref 56789
6. 文件结束标记(EOF)
- 文件结束标记:表示文件的结束。格式如下:
%%EOF
7.典型的 PDF 文件结构示例
%PDF-1.7
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /Resources <<>> /MediaBox [0 0 612 792] /Contents 4 0 R >>
endobj
4 0 obj
<< /Length 55 >>
stream
BT
/F1 18 Tf
0 0 Td
(Hello, World!) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000010 00000 n
0000000078 00000 n
0000000123 00000 n
0000000234 00000 n
trailer
<< /Size 5 /Root 1 0 R /Info 3 0 R >>
startxref
56789
%%EOF
复习一下
PDF 文件的结构包括文件头、主体、交叉引用表、文件尾、启动代码和文件结束标记。这些部分共同构成了一个完整的 PDF 文件,确保了文件的正确解析和显示。
怎么解析?
有点复杂,感觉碰到了知识盲区,那我们解析难道要通过流一个字节一个字节去解析文档吗?当然不需要,各个高级语言基本都实现了对PDF的解析,有对应的解析框架,而且大部分都是开源的。
当然,开源不收费,但是文档可能不友好,也有文档友好的,但是可能收费用,根据自己开发的实际情况来评估投产比进行选择,我们这篇文章不讨论选型,后续会有专门的文章来讨论。
设计一个解析器
我们可以尝试着设计一个解析器,因为 PDF 格式相当复杂,我们设计的目的是为了更好的了解PDF格式,同时能初窥一下成熟框架的设计思路,后面学习框架更容易上手。所以一定注意,下面的设计和代码不能用在实际生产中。
设计思路
- 读取文件:读取 PDF 文件的二进制内容。
- 解析文件头:识别 PDF 文件的版本和注释。
- 解析交叉引用表(XRef):找到对象的位置。
- 解析对象:读取并解析 PDF 文件中的对象。
- 提取文本内容:从解析的对象中提取文本内容。
代码实现
以下是一个简单的 Java 程序,用于解析 PDF 文件并提取文本内容。这个程序只实现了基本的功能,适用于简单的 PDF 文件。
1. 读取文件
import java.io.*;
import java.util.*;
public class SimplePDFParser {
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: java SimplePDFParser <path-to-pdf-file>");
System.exit(1);
}
String pdfFilePath = args[0];
File pdfFile = new File(pdfFilePath);
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(pdfFile))) {
byte[] buffer = new byte[BUFFER_SIZE];
StringBuilder content = new StringBuilder();
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
content.append(new String(buffer, 0, bytesRead));
}
parsePDF(content.toString());
} catch (IOException e) {
System.err.println("Error reading PDF file: " + e.getMessage());
e.printStackTrace();
}
}
private static void parsePDF(String content) {
// 解析文件头
parseHeader(content);
// 解析交叉引用表
Map<Integer, Integer> xrefTable = parseXRefTable(content);
// 解析对象
List<PDFObject> objects = parseObjects(content, xrefTable);
// 提取文本内容
extractText(objects);
}
private static void parseHeader(String content) {
String header = content.substring(0, 8);
System.out.println("PDF Header: " + header);
}
private static Map<Integer, Integer> parseXRefTable(String content) {
Map<Integer, Integer> xrefTable = new HashMap<>();
int xrefStart = content.indexOf("xref");
int trailerStart = content.indexOf("trailer", xrefStart);
String xrefSection = content.substring(xrefStart, trailerStart);
String[] lines = xrefSection.split("\n");
for (int i = 2; i < lines.length; i++) {
String line = lines[i].trim();
if (line.isEmpty()) continue;
String[] parts = line.split(" ");
int objectNumber = Integer.parseInt(parts[0]);
int offset = Integer.parseInt(parts[1]);
xrefTable.put(objectNumber, offset);
}
return xrefTable;
}
private static List<PDFObject> parseObjects(String content, Map<Integer, Integer> xrefTable) {
List<PDFObject> objects = new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : xrefTable.entrySet()) {
int objectNumber = entry.getKey();
int offset = entry.getValue();
int start = content.indexOf(objectNumber + " 0 obj", offset);
int end = content.indexOf("endobj", start);
String objectContent = content.substring(start, end).trim();
PDFObject pdfObject = new PDFObject(objectNumber, objectContent);
objects.add(pdfObject);
}
return objects;
}
private static void extractText(List<PDFObject> objects) {
for (PDFObject obj : objects) {
if (obj.getContent().contains("/Type /Page")) {
int streamStart = obj.getContent().indexOf("stream");
int streamEnd = obj.getContent().indexOf("endstream");
String streamContent = obj.getContent().substring(streamStart + 6, streamEnd).trim();
System.out.println("Text in Page Object " + obj.getObjectNumber() + ": " + streamContent);
}
}
}
static class PDFObject {
private int objectNumber;
private String content;
public PDFObject(int objectNumber, String content) {
this.objectNumber = objectNumber;
this.content = content;
}
public int getObjectNumber() {
return objectNumber;
}
public String getContent() {
return content;
}
}
}
代码解释
-
读取文件:
- 使用
BufferedInputStream
读取 PDF 文件的二进制内容,并将其转换为字符串。
- 使用
-
解析文件头:
- 从文件的前 8 个字符中提取 PDF 文件头,验证文件是否为 PDF 格式。
-
解析交叉引用表(XRef):
- 找到
xref
和trailer
关键字之间的部分,解析出对象的偏移量,并存储在xrefTable
中。
- 找到
-
解析对象:
- 根据
xrefTable
中的偏移量,找到每个对象的内容,并存储在PDFObject
对象中。
- 根据
-
提取文本内容:
- 遍历所有对象,找到类型为
Page
的对象,从中提取文本内容。
- 遍历所有对象,找到类型为
注意事项-再次提醒
- 简化:这个示例仅实现了最基本的功能,适用于简单的 PDF 文件。对于复杂的 PDF 文件,还需要处理更多的细节,例如压缩流、加密等。
- 性能:由于是逐字符读取和解析,性能可能不是最优。对于大文件,可以考虑优化读取和解析过程。
- 错误处理:示例代码中没有详细的错误处理,实际应用中需要增加更多的异常处理和容错机制。
成熟框架-Java解析框架 PDFBox
当当当,生产环境使用,还是得看成熟框架,这些框架经过了长时间的迭代和大量实际生产的考验,我们从头手撸一个不是不可以,但是咱没那时间也扛不住那成本,除非是碰到了特殊的需求(比如:easyexcel就是碰到了POI大数据量导入导出性能问题,才诞生的),一般情况下,成熟框架都能满足需求,且稳定和高效。
那我们就来了解下Java的PDF解析框架-Apache PDFBox 。Apache PDFBox是一个开源的 Java 库,用来处理PDF文件,主要功能如下:
主要功能
-
创建 PDF 文档:
- 可以从头开始创建新的 PDF 文档,并添加文本、图像、表格等内容。
-
解析 PDF 文档:
- 能够读取现有的 PDF 文件,提取其中的文本、图像和其他内容。
-
修改 PDF 文档:
- 支持对现有的 PDF 文件进行修改,如添加水印、签名、页眉和页脚等。
-
加密和解密 PDF 文档:
- 可以对 PDF 文件进行加密,设置访问权限,也可以解密已加密的 PDF 文件。
-
表单处理:
- 支持填充和导出 PDF 表单数据。
-
元数据处理:
- 可以读取和修改 PDF 文件的元数据信息。
使用示例
创建 PDF 文档
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
public class CreatePDF {
public static void main(String[] args) {
try (PDDocument document = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.beginText();
contentStream.newLineAtOffset(100, 700);
contentStream.showText("Hello, PDFBox!");
contentStream.endText();
}
document.save("output.pdf");
} catch (Exception e) {
e.printStackTrace();
}
}
}
解析 PDF 文档
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
public class ParsePDF {
public static void main(String[] args) {
try (PDDocument document = PDDocument.load(new File("input.pdf"))) {
PDFTextStripper pdfStripper = new PDFTextStripper();
String text = pdfStripper.getText(document);
System.out.println(text);
} catch (Exception e) {
e.printStackTrace();
}
}
}
修改 PDF 文档
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
public class ModifyPDF {
public static void main(String[] args) {
try (PDDocument document = PDDocument.load(new File("input.pdf"))) {
PDPage page = document.getPage(0);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true)) {
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.beginText();
contentStream.newLineAtOffset(100, 650);
contentStream.showText("This is an additional line.");
contentStream.endText();
}
document.save("modified.pdf");
} catch (Exception e) {
e.printStackTrace();
}
}
}
依赖管理
如果你使用 Maven 管理项目依赖,可以在 pom.xml
中添加以下依赖(3.0.3应该是目前最新版本):
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
后面会持续介绍PDFBox的各种深入的应用。