pdf2docx开发概要:解析页面布局¶
发布于:2021-05-30 | 分类:process automation
前文介绍了从PDF直接提取出的基本数据:块元素Block
和形状Shape
,但是它们目前还只有位置数据,接下来要做的是解析语义数据,例如从块元素解析出表格和普通段落,从形状元素解析出文本样式例如下划线、高亮及其作用的文本、解析表格样式如边框颜色、背景色及其包含的段落。我们先从页面布局开始。
数据结构¶
为了更好地管理和解析Block
和Shape
,并且考虑最后在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_width
、left_margin
。于是页面基本形式得以确定。
分栏¶
早期版本通过表格来实现分栏布局,从0.5.2版开始引入Section
和Column
,以保证页面逻辑的合理性,同时减少嵌套表格的泛滥。
注意
为兼顾通用性和降低复杂度,目前仅支持单栏或者两栏的布局,更多的分栏将被解析为表格。
分栏逻辑¶
自上而下检测每一行,将连续的单列或者双列归为同一个Section
,其中每一列即为一个Column
。同时,注意以下细节:
-
目前最多考虑两栏,故列数大于2的行视为单栏
-
当前行可以分为两列时,
- 当其中一列宽度很小例如小于5Pt,则视为单栏;
- 前一个
Section
也是两栏,但是各自的栏分隔线不重合,则当前行视为单栏
-
当前行仅为一列,且前一个
Section
为两栏时,- 当前行完全处于前一个
Section
的左栏,则当前行视为两栏(右栏为空); - 前一个
Section
高度较小例如小于20Pt,则前一个Section
退化为一栏
- 当前行完全处于前一个
竖直定位¶
每一个Section
在页面竖直方向的位置 由前一个段落的段后间距 确定,因此在确定好Section
后,计算当前Section
开始位置y0
与前一个Section
结束位置y1
的差值,作为Section
的一个属性before_space
。
使用python-docx
重建Section
时,设置前一个Section
最后一个段落的段后距离等于当前Section
的before_space
即可。两个Column
之间设置列分隔符WD_SECTION.NEW_COLUMN
。
每一页第一个Section
的处理
- 计算
before_space
时,前一个参考位置为页面上边距。 - 重建
Section
时,新建一个空段落作为设置段后间距的参考。
表格和段落解析¶
创建Section
后,将原始的Block
和Shape
按照位置关系分配到相应的Column
中去,以便进一步解析表格和段落。Column
是一个布局对象,直接容纳Block
和Shape
,所以pdf2docx
为其抽象出一个Layout
类,便于管理和操作。
┌────────────────────────────────┐
│Layout │
│ ┌────────────┐ ┌───────────┐ │
│ │Blocks │ │Shapes │ │
│ │ TextBlock │ │ Stroke │ │
│ │ TableBlock│ │ Fill │ │
│ │ ... │ │ ... │ │
│ └────────────┘ └───────────┘ │
└────────────────────────────────┘
PDF直接提取出的块元素只有TextBlock
,结合形状元素(潜在的表格边框、单元格背景色)解析出TableBlock
后,继续根据位置关系将表格范围内的Block
和Shape
分配到相应的单元格中去。由此可知,容纳Block
和Shape
的单元格也是一个Layout
对象,可以继续进行单元格内的嵌套表格和段落解析。这样的设计确保可以递归解析出无限嵌套的表格,同时也有助于将PDF的浮动布局转化为流动布局,便于在Word中重建。
至于表格的具体解析方法,单开一篇进行具体介绍:
最后,总结页面布局级别的数据结构及基本属性如下:
# 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
}
]
}