pdf2docx开发概要:解析页面布局

发布于:2021-05-30 | 分类:process automation


前文介绍了从PDF直接提取出的基本数据:块元素Block和形状Shape,但是它们目前还只有位置数据,接下来要做的是解析语义数据,例如从块元素解析出表格和普通段落,从形状元素解析出文本样式例如下划线、高亮及其作用的文本、解析表格样式如边框颜色、背景色及其包含的段落。我们先从页面布局开始。

数据结构

为了更好地管理和解析BlockShape,并且考虑最后在Word中的重建,设计pdf2docx转换类的基本结构如下。其中:

  • Page类对应物理上的一个页面,具有尺寸、边距、页眉页脚等属性,主要内容按照流式布局顺序被分为一个或者多个Section

  • Section对应Word的节(Section),表示具有类似结构的布局,例如一个或者多个分栏Column

  • Column对应Word的分栏(Column),表示分栏中的一列。对于普通的排版,块元素和形状元素都在一个Section的一个Column

┌───────────────────────────────────┐
│pdf2docx Converter                 │
│ ┌───────────────────────────────┐ │
│ │Page-1                         │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │Section-1                  │ │ │
│ │ │ ┌──────────┐ ┌──────────┐ │ │ │
│ │ │ │ Column-1 │ │ Column-2 │ │ │ │
│ │ │ └──────────┘ └──────────┘ │ │ │
│ │ └───────────────────────────┘ │ │
│ │  Section-2                    │ │
│ └───────────────────────────────┘ │
│  Page-2                           │
└───────────────────────────────────┘

页眉页脚

理论上,无法从单个页面识别出页眉和页脚,所以需要从文档级别即多个Pages一起对比结构相似性,从而从页面开始部分分离出页眉、从页面结束部分分离出页脚。同时,需要处理一些不确定因素,例如页码、奇偶页不同的页眉页脚。

截至v0.5.2,这部分依然占坑待处理中。

页面大小与页边距

PyMuPDF提取的数据直接包括了页面宽度和高度,然后根据 除去页眉页脚后 的所有块级、形状元素所占据的区域计算页边距,例如最小左上角点确定了左边距和上边距。

python-docx中页面section对象恰好提供了这六个属性,例如page_widthleft_margin。于是页面基本形式得以确定。

分栏

早期版本通过表格来实现分栏布局,从0.5.2版开始引入SectionColumn,以保证页面逻辑的合理性,同时减少嵌套表格的泛滥。

注意

为兼顾通用性和降低复杂度,目前仅支持单栏或者两栏的布局,更多的分栏将被解析为表格。

分栏逻辑

自上而下检测每一行,将连续的单列或者双列归为同一个Section,其中每一列即为一个Column。同时,注意以下细节:

  • 目前最多考虑两栏,故列数大于2的行视为单栏

  • 当前行可以分为两列时,

    • 当其中一列宽度很小例如小于5Pt,则视为单栏;
    • 前一个Section也是两栏,但是各自的栏分隔线不重合,则当前行视为单栏
  • 当前行仅为一列,且前一个Section为两栏时,

    • 当前行完全处于前一个Section的左栏,则当前行视为两栏(右栏为空);
    • 前一个Section高度较小例如小于20Pt,则前一个Section退化为一栏

竖直定位

每一个Section在页面竖直方向的位置 由前一个段落的段后间距 确定,因此在确定好Section后,计算当前Section开始位置y0与前一个Section结束位置y1的差值,作为Section的一个属性before_space

使用python-docx重建Section时,设置前一个Section最后一个段落的段后距离等于当前Sectionbefore_space即可。两个Column之间设置列分隔符WD_SECTION.NEW_COLUMN

每一页第一个Section的处理

  • 计算before_space时,前一个参考位置为页面上边距。
  • 重建Section时,新建一个空段落作为设置段后间距的参考。

表格和段落解析

创建Section后,将原始的BlockShape按照位置关系分配到相应的Column中去,以便进一步解析表格和段落。Column是一个布局对象,直接容纳BlockShape,所以pdf2docx为其抽象出一个Layout类,便于管理和操作。

┌────────────────────────────────┐
│Layout                          │
│ ┌────────────┐   ┌───────────┐ │
│ │Blocks      │   │Shapes     │ │
│ │  TextBlock │   │  Stroke   │ │
│ │  TableBlock│   │  Fill     │ │
│ │  ...       │   │  ...      │ │
│ └────────────┘   └───────────┘ │
└────────────────────────────────┘

PDF直接提取出的块元素只有TextBlock,结合形状元素(潜在的表格边框、单元格背景色)解析出TableBlock后,继续根据位置关系将表格范围内的BlockShape分配到相应的单元格中去。由此可知,容纳BlockShape的单元格也是一个Layout对象,可以继续进行单元格内的嵌套表格和段落解析。这样的设计确保可以递归解析出无限嵌套的表格,同时也有助于将PDF的浮动布局转化为流动布局,便于在Word中重建。

至于表格的具体解析方法,单开一篇进行具体介绍:

pdf2docx开发概要:解析表格

最后,总结页面布局级别的数据结构及基本属性如下:

# page -> section -> column
{
    "filename": "demo.pdf",
    "page_cnt": 2,
    "pages": [
        {
            "id": 0,
            "width": float,
            "height": float,
            "margin": [float, float, float, float],
            "sections": [
                {
                    "bbox": [float, float, float, float],
                    "cols": int,
                    "space": float,
                    "before_space": float,
                    "columns": [
                        {
                            "bbox": [float, float, float, float],
                            "blocks": [],
                            "shapes": []
                        },
                        {
                            # column 2
                        }
                    ]
                },
                {
                    # section 2
                }
            ],
            "header": str,
            "footer": str,
            "floats": []
        },
        {
            # page 2
        }
    ]
}