当前位置: 首页 > article >正文

【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 个字符是对象的版本号,最后一个字符 fn 表示对象的状态(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格式,同时能初窥一下成熟框架的设计思路,后面学习框架更容易上手。所以一定注意,下面的设计和代码不能用在实际生产中。

设计思路

  1. 读取文件:读取 PDF 文件的二进制内容。
  2. 解析文件头:识别 PDF 文件的版本和注释。
  3. 解析交叉引用表(XRef):找到对象的位置。
  4. 解析对象:读取并解析 PDF 文件中的对象。
  5. 提取文本内容:从解析的对象中提取文本内容。

代码实现

以下是一个简单的 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;
        }
    }
}

代码解释

  1. 读取文件

    • 使用 BufferedInputStream 读取 PDF 文件的二进制内容,并将其转换为字符串。
  2. 解析文件头

    • 从文件的前 8 个字符中提取 PDF 文件头,验证文件是否为 PDF 格式。
  3. 解析交叉引用表(XRef)

    • 找到 xreftrailer 关键字之间的部分,解析出对象的偏移量,并存储在 xrefTable 中。
  4. 解析对象

    • 根据 xrefTable 中的偏移量,找到每个对象的内容,并存储在 PDFObject 对象中。
  5. 提取文本内容

    • 遍历所有对象,找到类型为 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的各种深入的应用。


http://www.kler.cn/a/404774.html

相关文章:

  • 掌握移动端性能测试利器:深入JMeter手机录制功能
  • 不需要双手离开键盘 vscode
  • 输出比较简介
  • maven父子项目
  • 算法学习笔记(六):二叉树一创建、插入、删除、BFS
  • Linux驱动开发(9):pinctrl子系统和gpio子系统--led实验
  • Java八股-MyBatis延迟加载
  • 提交git仓库时,如何关闭lint校验
  • 数据结构 (1)基本概念和术语
  • Easyexcel(4-模板文件)
  • 【QT - 1 - 】什么是QT?
  • LeetCode —— 字母异位词分组
  • Linux 定时任务全解析
  • Spring Cloud Alibaba、Spring Cloud 与 Spring Boot各版本的对应关系
  • 【docker】docker commit 命令 将当前容器的状态保存为一个新的镜像
  • RK3588开发笔记-sata概率性不能识别问题解决
  • 05_Spring JdbcTemplate
  • 【软件开发】如何理解异地多活?
  • 网络安全的学习路线
  • mysql安装---rpm包
  • Flutter-Web首次加载时添加动画
  • [STM32]从零开始的STM32 HAL库环境搭建
  • NuGet如何支持HTTP源
  • 【鸿蒙开发】基础干货篇--6 “超简单持久化存储PersistentStorage”
  • 汽车资讯新视界:Spring Boot技术启航
  • 期权懂|期权中的行权和平仓的区别在于哪里?