Skip to content

Commit

Permalink
update posts: pdf2docx technical documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
dothinking committed Jun 1, 2021
2 parents cd28958 + 3b1d69b commit 4316a55
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 99 deletions.
31 changes: 24 additions & 7 deletions docs/2020-07-13-pdf2docx开发概要.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ tags: [python]

**PDF转Word** 是一个古老的话题,其难点在于建立PDF基于元素位置的格式与Word基于内容的格式之间的映射关系。[`Solid Documents`](https://solidframework.net/)是这方面的佼佼者,其技术的应用案例:在线PDF转换网站[Smallpdf](https://smallpdf.com/pdf-to-word)

在某个项目的调研过程中,作者尝试了这个话题,编写了一个用于转换PDF到Word的Python库`pdf2docx`——依赖`PyMuPDF`解析PDF文件,并用`python-docx`创建Word文件。本文记录相关开发思路(一些特性仅针对如下列出版本而言)。
在某个项目的调研过程中,作者尝试了这个话题,编写了一个用于转换PDF到Word的Python库`pdf2docx`——借助`PyMuPDF`从PDF文件提取内容,基于位置规则解析内容,最后用`python-docx`创建Word文件。

> https://github.com/dothinking/pdf2docx
本文记录主要开发思路,具体细节随着版本升级可能略有差异。

![sample](https://camo.githubusercontent.com/a581ee7caccdcf093648d54445fcc689a32ef205a81594c94b2e410cbde07757/68747470733a2f2f73312e617831782e636f6d2f323032302f30382f30342f6144727978312e706e67)

- 项目地址:https://github.com/dothinking/pdf2docx
- 当前版本:v0.4.3
- 依赖库版本:`PyMuPDF v1.17.3``python-docx v0.8.10`


## 思路
Expand All @@ -27,19 +30,33 @@ tags: [python]

以上技术路线也决定了`pdf2docx`的局限:

- 只能处理标准格式的PDF,不支持扫描版、图片格式PDF。

- 只能处理水平、竖直方向文本,忽略旋转角度的文本。

- 根据有限的、确定的规则建立PDF导出元素位置与docx要求的内容之间的映射并非完全可靠,也就是说仅能处理常见的规范的格式,而非百分百还原。

- Word格式还受到`python-docx`处理能力的限制,例如截至版本`0.8.10``python-docx`尚不支持浮动图片。


## 目录

以下分篇介绍提取PDF页面数据、解析和重建docx过程中的具体细节:

- [提取文本、图片和形状](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)
- [解析文本样式](2020-07-20-pdf2docx开发概要:解析文本样式.md)

- [获取图片及其位置](2020-10-15-pdf2docx开发概要:获取图片及其位置.md)
- [创建浮动图片](2020-10-25-pdf2docx开发概要:创建浮动图片.md)
- [矢量图处理](2020-10-01-pdf2docx开发概要:矢量图处理.md)

- [解析页面布局](2021-05-30-pdf2docx开发概要:解析页面布局.md)

- [解析表格](2020-08-15-pdf2docx开发概要:解析表格.md)
- [解析页面布局](2020-08-27-pdf2docx开发概要:解析页面布局.md)

- [对齐隐式表格线](2020-09-27-pdf2docx开发概要:对齐隐式表格线.md)

- [解析段落](2020-08-27-pdf2docx开发概要:解析段落.md)

- [解析文本样式](2020-07-20-pdf2docx开发概要:解析文本样式.md)


[^1]: [PDF Reference 1.7](https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdf_reference_archive/pdf_reference_1-7.pdf)
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ tags: [python]

![文本与图片块结构](https://pymupdf.readthedocs.io/en/latest/_images/img-textpage.png)

`pdf2docx`继续沿用以上数据结构,并稍作改动:
`pdf2docx`继续沿用以上数据结构,并稍作改动,作为第一类基本数据`Block`

- 将图片块整合到文本块中,作为`line`>`span`级别的元素
- 将图片块整合到文本块`TextBlock`中,作为`line`>`span`级别的元素

- 增加布局参数,例如`alignment``left_space``before_space`,用以保存后续段落布局解析结果

- `block`元素字典增加了页面布局参数,例如`alignment``left_space``before_space`,用以保存后续页面布局解析结果

- 新增了表格块,用以保存表格解析结果

!!! warning "注意"
后续版本中发现`extractDICT()`获取图片存在问题(如只能获取完全显示在页面中的图片、丢失alpha通道),还需配合`page.getImageList()`处理;另外,v0.5.0版本中对浮动图片也进行了支持,详见以下两篇图片相关的记录。
Expand All @@ -35,7 +35,9 @@ tags: [python]

## 形状

文本与图片构成了主体内容,它们的样式则由 **形状** 来描述,例如代表文本高亮的矩形块,表明表格边线的直线(很细的矩形)。这里的所谓的形状具体指两类来源:
文本与图片构成了主体内容,它们的样式则由 **形状** 来描述,例如代表文本高亮的矩形块,表明表格边线的直线(很细的矩形)。形状构成了`pdf2docx`的第二类基本数据`Shape`

这里的所谓的形状具体指两类来源:

- PDF原始文件中的路径`Path`

Expand Down Expand Up @@ -150,6 +152,13 @@ EMC
`Hyperlink`类似于`Stroke`,本质区别在于`Hyperlink`的语义类型是已知的。更一般地理解,`Stroke`是一种几何类型,事先并不知道它的语义类型(边框、删除线还是下划线?);而`Hyperlink`是一种确定了语义类型的`Stroke`


## 总结

借助`PyMuPDF`从PDF提取文本、图片和形状数据,它们构成了`pdf2docx`的两类基础数据:

- 块元素`Block`,代表主体内容,例如直接提取出的文本/图片组成的`TextBlock`,以及后续解析得到的表格结构`TableBlock`

- 形状元素`Shape`,代表格式内容,例如描边`Stroke`将对应下划线、表格边框等,填充`Fill`将对应文本高亮、单元格背景色等。


[^1]: [TextPage](https://pymupdf.readthedocs.io/en/latest/textpage.html)
Expand Down
4 changes: 2 additions & 2 deletions docs/2020-07-20-pdf2docx开发概要:解析文本样式.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ tags: [python]
在原始文本块数据结构的基础上,为`span`块新增表征解析结果的`style`属性:

```python
# dict of span
# span
{
"bbox": (float, float, float, float)
"bbox": [float, float, float, float],
"size": float,
"flags": int,
"font": str,
Expand Down
14 changes: 9 additions & 5 deletions docs/2020-08-15-pdf2docx开发概要:解析表格.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,14 @@ PDF中没有语义上的表格的概念,所以需要根据外观上的表格

## 数据结构

参考文本和图片,定义表格类型的数据结构:
表格块`TableBlock`继承了块元素`Block`的结构(详见[PDF提取](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)),同时新增了表征表格结构的`Row``Cell`。其中`Cell`是布局级别的对象(`Layout`),嵌套了下一级的块元素和形状元素,如此递归,直到块元素中不再包含表格,即只有表征文本和图片的`TextBlock`


```python
{
'type': int, # 3-explicit table; 4-implicit table
'bbox': (float, float, float, float),
..., # some spacing properties
..., # some spacing properties same with Block
"rows": [
{
'bbox': (float, float, float, float),
Expand All @@ -103,9 +104,12 @@ PDF中没有语义上的表格的概念,所以需要根据外观上的表格
'border-width': (float, float, float, float),
'merged-cells': (int, int),
'blocks': [
{
# text/image blocks contained in current cell
}
{ # text/image blocks contained in current cell },
{ ... }
],
'shapes': [
{ # shapes contained in current cell }
{ ... }
]
},
... # more cells
Expand Down
139 changes: 139 additions & 0 deletions docs/2020-08-27-pdf2docx开发概要:解析段落.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
categories: [process automation]
tags: [python]
---

# pdf2docx开发概要:解析段落


---


经过[表格解析](2020-08-15-pdf2docx开发概要:解析表格.md)后,我们得到了整合的块元素:文本/图片块`TextBlock`和表格块`TableBlock`。其中文本/图片块将被重建为段落,表格块将被重建为表格,单元格内的块元素按照相同的逻辑进行递归处理。在此基础上,计算相邻元素之间的间距,例如竖直方向的前后段间距、水平方向的段落缩进。



## 竖直方向定位:段间距

块级元素之间通过间距确定相对位置关系。由于Word中 **段落具有段前/段后间距属性**,文本块将被作为定位的参考元素。

竖直间距确定原则:

- 考察竖直方向上相邻的两个块级元素,前一个是参考块,后一个是当前块。对于第一个块级元素,参考块是上边距,当前块即为自身。

- 如果当前是文本块或者图片块(不论参考块是文本、图片还是表格),则设置当前块的段前间距`before_space`为二者之间垂直距离。

- 如果当前块是表格块,则考察参考块:
- 如果参考块是文本块或者图片块(此时当前块为表格),则设置参考块的段后距离`after_space`
- 如果参考块还是表格块,则设置当前块的段前间距`before_space`


!!! warning "注意"
- `docx`中无法直接设置两个表格的间距,创建表格时采用变通方式:在间距中插入空文本块,然后设置该文本块的段前间距。
- 如果表格在末尾,`MS Word`会自动加一个标准间距的空段落,这样可能导致非预期的换页。因此,此种情况注意人为添加一个空段落并设置其为最小的间距值,例如前后零间距,行高1磅。


## 竖直方向定位:行间距

已知块的高度和内部行数,容易计算得到平均行距。Word中有两种设置行间距的方式:

- 固定值:直接设置为计算出的平均行距,优点是定位精确,缺点是不会随着字号的变化而改变,不利于编辑。例如一旦增大字号,则有可能导致该行文本显示不全。

- 倍数行距:与单倍行距的比值,优缺点刚好与固定行距相反。

综合来看,倍数行距有更好的适应性,v0.5.2版开始启用倍数行距。

注意,倍数行距并非行高与字号的简单比值或者流传的1.2倍的比例关系,而是 **与具体字体相关**。单开一篇介绍倍数行距计算问题:

> [此坑待填...](to_do)

## 水平方向定位:对齐方式与缩进

水平方向从内部(块内元素`Line`的对齐关系)和外部(页面中的位置)两个方面确定对齐方式:左/居中/右/分散对齐。其中左对齐为默认方式,因为结合段落左缩进和制表符,总能正确定位任何块间元素。

**内部对齐关系**:行与行之间位置关系

- 如果块内有不连续的行(相邻`line`存在明显的间距),则设为左对齐,以便结合制表符定位
- 如果只有一行,则参考外部对齐关系
- 判断各行左边距、右边距、中心距离差值是否小于指定值,即是否对齐:
- 左、右都对齐:如果行数不少于3行,则为分散对齐,否则不能确定,需要进一步参考 外部对齐关系;
- 否则,依次按左对齐、右对齐、中心对齐判断下去

!!! warning "注意"
- 判断左对齐时注意排除第一行,因为第一行可能缩进或悬挂缩进;如果满足左对齐,计算第一行的缩进量(负值表示悬挂缩进)。
- 判断右对齐时注意排除最后一行,目的是考虑分散对齐——最后一行可能不满,但依旧满足分散对齐


**外部对齐关系**:块在页面中的位置

分别计算块与页面的边距:左边距、右边距、中心距离差值,然后顺序判断

- 块中心与页面中心差值很小 -> 居中对齐
- 依次判断左、右边距差值,哪个差值小即为相应对齐方式


最后,创建`docx`时通过设置段落的左/右缩进和对齐方式来实现。并且,左对齐方式还要通过 **制表位** 来保证段落内不同行的水平位置。


!!! warning "重建docx时的一些优化处理"
- 同时设置左、右边距将严格限定段落的水平位置,可能导致意外的换行。因此,可以适当放宽“对面”的间距:左对齐时放宽(减小)右边距,右对齐放宽左边距,居中对齐则同时放宽左、右边距。
- 如果只有一行,则将这个放宽放到极限,即设置相应边距等于0。


## 文本样式

以上解析结果确定了段落在页面中的位置和呈现样式,接下来深入到段内文本。`PyMuPDF`提取的原始文本块自带了字体、颜色、斜体、粗体等属性,但是`高亮``下划线``删除线`等具体样式需要进一步根据 **文本和形状的位置关系** 来判定。

具体参考下文:

> [pdf2docx开发概要:解析文本样式](2020-07-20-pdf2docx开发概要:解析文本样式.md)
## 数据结构

综上,文本块`TextBlock`在标准`Block`数据结构(`Line`->`Span`->`Char`)的基础上,引入了如下定位相关属性:

```python
# text block
{
"type": 0,
"bbox": [float, float, float, float],

# ----- vertical spacing -----
"before_space": float,
"line_space": float,
"after_space": float

# ----- horizontal spacing -----
"alignment": int,
"left_space": float,
"right_space": float,
"first_line_space": float,
"tab_stops": [float, float, ...],

"lines": [
{
"bbox": [float, float, float, float],
"wmode": int,
"dir": [float, float],
"line_break": int, # new property
"tab_stop": int, # new property
"spans": [
{
"bbox": [float, float, float, float],
"color": int,
"font": str,
"size": float,
"flags": int,
"text": str,
"chars": [{
"bbox": [float, float, float, float],
"c": str
}]
}
]
}
]
}
```

80 changes: 0 additions & 80 deletions docs/2020-08-27-pdf2docx开发概要:解析页面布局.md

This file was deleted.

Loading

0 comments on commit 4316a55

Please sign in to comment.