From 7072fcbae719351ece80547ac09ee987efc9ffd4 Mon Sep 17 00:00:00 2001 From: "yunjian.wu" Date: Fri, 16 Apr 2021 16:28:54 +0800 Subject: [PATCH 1/3] update post: parsing layout --- ...65\351\235\242\345\270\203\345\261\200.md" | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" index 68da353..14496f8 100644 --- "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" +++ "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" @@ -18,15 +18,15 @@ tags: [python] `python-docx`中页面`section`对象恰好提供了这六个属性,例如`page_width`、`left_margin`。于是页面基本形式得以确定。 -## 竖直方向定位 +## 竖直方向定位:段间距 -块级元素之间通过段前/段后间距确定相对位置关系,同时保证自身的高度,例如段落行高对于文本块,表格行高对于表格块。由于Word中段落具有段前/段后间距属性,文本块将被作为定位的参考元素。竖直间距确定原则: +块级元素之间通过段前/段后间距确定相对位置关系。由于Word中段落具有段前/段后间距属性,文本块将被作为定位的参考元素。 + +竖直间距确定原则: - 考察竖直方向上相邻的两个块级元素,前一个是参考块,后一个是当前块。对于第一个块级元素,参考块是上边距,当前块即为自身。 - - 如果当前是文本块或者图片块(不论参考块是文本、图片还是表格),则设置当前块的段前间距`before_space`为二者之间垂直距离。同时, - - 如果当前是文本块,则设置行高`line_space`为平均磅数:文本块高度除以物理行数 - - 如果当前是图片块,则无需显式设置行高,docx创建时直接设置1.05倍行距即可。 + - 如果当前是文本块或者图片块(不论参考块是文本、图片还是表格),则设置当前块的段前间距`before_space`为二者之间垂直距离。 - 如果当前块是表格块,则考察参考块: - 如果参考块是文本块或者图片块(此时当前块为表格),则设置参考块的段后距离`after_space` @@ -37,23 +37,52 @@ tags: [python] - `docx`中无法直接设置两个表格的间距,创建表格时采用变通方式:在间距中插入空文本块,然后设置该文本块的段前间距。 - 如果表格在末尾,`MS Word`会自动加一个标准间距的空段落,这样可能导致非预期的换页。因此,此种情况注意人为添加一个空段落并设置其为最小的间距值,例如前后零间距,行高1磅。 + +## 竖直方向定位:行间距 + +已知块的高度和内部行数,容易计算得到平均行距。Word中有两种设置行间距的方式: + +- 固定值:即为计算得到的平均行距,优点是定位精确,缺点是不会随着字体大小修改而改变,不利于编辑。 +- 倍数行距:与单倍行距的比值,优缺点与固定值行距正好相反 + +综合来看,倍数行距有更好的适应性,但是 **这个值的计算和字体相关**,单开一篇进行具体介绍: + +> [TODO](to_do) + -## 水平方向定位 +## 水平方向定位:对齐方式与缩进 -水平方向根据块内元素的对齐关系以及在页面中的位置确定对齐方式:左/居中/右/分散对齐。其中左对齐为默认方式,因为结合段落左缩进和制表符,总能正确定位任何块间元素。 +水平方向从内部(块内元素的对齐关系)和外部(页面中的位置)两个方面确定对齐方式:左/居中/右/分散对齐。其中左对齐为默认方式,因为结合段落左缩进和制表符,总能正确定位任何块间元素。 + +**内部对齐关系**:行与行之间位置关系 - 如果块内有不连续的行(相邻`line`存在明显的间距),则设为左对齐,以便结合制表符定位 -- 如果只有一行,则考察与页面的关系: - - 这一行足够长 -> 左对齐 - - 行中心与页面中心差值很小 -> 居中对齐 - - 依次判断左、右边距差值,哪个差值小即为相应对齐方式 -- 如果存在多行,则考察这些行之间的对齐关系: - - 中心对齐其左右都不对齐 -> 中心对齐 +- 如果只有一行,则参考外部对齐关系 +- 判断各行左边距、右边距、中心距离差值是否小于指定值,即是否对齐: + - 左、右都对齐:如果行数不少于3行,则为分散对齐,否则不能确定,需要进一步参考 外部对齐关系; + - 否则,依次按左对齐、右对齐、中心对齐判断下去 + +!!! warning "注意" + - 判断左对齐时注意排除第一行,因为第一行可能缩进或悬挂缩进;如果满足左对齐,计算第一行的缩进量(负值表示悬挂缩进)。 + - 判断右对齐时注意排除最后一行,目的是考虑分散对齐——最后一行可能不满,但依旧满足分散对齐 + + +**外部对齐关系**:块在页面中的位置 + +分别计算块与页面的边距:左边距、右边距、中心距离差值,然后顺序判断 + +- 块中心与页面中心差值很小 -> 居中对齐 +- 依次判断左、右边距差值,哪个差值小即为相应对齐方式 最后,创建`docx`时通过设置段落的左/右缩进和对齐方式来实现。并且,左对齐方式还要通过 **制表位** 来保证段落内不同行的水平位置。 +!!! warning "重建docx时的一些优化处理" + - 同时设置左、右边距将严格限定段落的水平位置,可能导致意外的换行。因此,可以适当放宽“对面”的间距:左对齐时放宽(减小)右边距,右对齐放宽左边距,居中对齐则同时放宽左、右边距。 + - 如果只有一行,则将这个放宽放到极限,即设置相应边距等于0。 + + ## 数据结构 综上,文本/表格块在标准数据结构的基础上,加入了如下定位相关属性: @@ -77,4 +106,5 @@ tags: [python] "right_space": float, "tab_stops": [float, float, ...] } -``` \ No newline at end of file +``` + From b19bd38731f2b9d887a1eb5f8282851104398e6f Mon Sep 17 00:00:00 2001 From: "yunjian.wu" Date: Tue, 1 Jun 2021 18:56:08 +0800 Subject: [PATCH 2/3] update posts: section/column --- ...00\345\217\221\346\246\202\350\246\201.md" | 31 +++- ...07\345\222\214\345\275\242\347\212\266.md" | 19 +- ...07\346\234\254\346\240\267\345\274\217.md" | 4 +- ...43\346\236\220\350\241\250\346\240\274.md" | 14 +- ...43\346\236\220\346\256\265\350\220\275.md" | 55 ++++-- ...65\351\235\242\345\270\203\345\261\200.md" | 162 ++++++++++++++++++ 6 files changed, 249 insertions(+), 36 deletions(-) rename "docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" => "docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" (66%) create mode 100644 "docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" diff --git "a/docs/2020-07-13-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201.md" "b/docs/2020-07-13-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201.md" index 7b83717..22e413b 100644 --- "a/docs/2020-07-13-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201.md" +++ "b/docs/2020-07-13-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201.md" @@ -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` ## 思路 @@ -27,9 +30,12 @@ tags: [python] 以上技术路线也决定了`pdf2docx`的局限: +- 只能处理标准格式的PDF,不支持扫描版、图片格式PDF。 + +- 只能处理水平、竖直方向文本,忽略旋转角度的文本。 + - 根据有限的、确定的规则建立PDF导出元素位置与docx要求的内容之间的映射并非完全可靠,也就是说仅能处理常见的规范的格式,而非百分百还原。 -- Word格式还受到`python-docx`处理能力的限制,例如截至版本`0.8.10`,`python-docx`尚不支持浮动图片。 ## 目录 @@ -37,9 +43,20 @@ tags: [python] 以下分篇介绍提取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) \ No newline at end of file diff --git "a/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" "b/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" index d754d1a..1b047f2 100644 --- "a/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" +++ "b/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" @@ -17,13 +17,13 @@ tags: [python] ![文本与图片块结构](https://pymupdf.readthedocs.io/en/latest/_images/img-textpage.png) -`pdf2docx`继续沿用以上数据结构,并稍作改动: +`pdf2docx`继续沿用以上数据结构,并稍作改动,作为第一类基本数据`Block`: -- 将图片块整合到文本块中,作为`line`>`span`级别的元素 +- 将图片块整合到文本块`TextBlock`中,作为`line`>`span`级别的元素 -- `block`元素字典增加了页面布局参数,例如`alignment`,`left_space`,`before_space`,用以保存后续页面布局解析结果 +- `block`增加了布局参数,例如`alignment`,`left_space`,`before_space`,用以保存后续段落布局解析结果 -- 新增了表格块,用以保存表格解析结果 +- 新增了表格块`TableBlock`,用以保存表格解析结果 !!! warning "注意" 后续版本中发现`extractDICT()`获取图片存在问题(如只能获取完全显示在页面中的图片、丢失alpha通道),还需配合`page.getImageList()`处理;另外,v0.5.0版本中对浮动图片也进行了支持,详见以下两篇图片相关的记录。 @@ -35,7 +35,9 @@ tags: [python] ## 形状 -文本与图片构成了主体内容,它们的样式则由 **形状** 来描述,例如代表文本高亮的矩形块,表明表格边线的直线(很细的矩形)。这里的所谓的形状具体指两类来源: +文本与图片构成了主体内容,它们的样式则由 **形状** 来描述,例如代表文本高亮的矩形块,表明表格边线的直线(很细的矩形)。形状构成了`pdf2docx`的第二类基本数据`Shape`。 + +这里的所谓的形状具体指两类来源: - PDF原始文件中的路径`Path` @@ -150,6 +152,13 @@ EMC `Hyperlink`类似于`Stroke`,本质区别在于`Hyperlink`的语义类型是已知的。更一般地理解,`Stroke`是一种几何类型,事先并不知道它的语义类型(边框、删除线还是下划线?);而`Hyperlink`是一种确定了语义类型的`Stroke`。 +## 总结 + +借助`PyMuPDF`从PDF提取文本、图片和形状数据,它们构成了`pdf2docx`的基础数据: + +- `Block`代表主体内容,例如文本/图片组成的`TextBlock`和表格结构的`TableBlock`,它们都将作为Word中的段落。 + +- `Shape`代表格式内容,例如描边`Stroke`将对应下划线、表格边框等,填充`Fill`将对应文本高亮、单元格背景色等。 [^1]: [TextPage](https://pymupdf.readthedocs.io/en/latest/textpage.html) diff --git "a/docs/2020-07-20-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\226\207\346\234\254\346\240\267\345\274\217.md" "b/docs/2020-07-20-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\226\207\346\234\254\346\240\267\345\274\217.md" index 68b309d..f09e921 100644 --- "a/docs/2020-07-20-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\226\207\346\234\254\346\240\267\345\274\217.md" +++ "b/docs/2020-07-20-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\226\207\346\234\254\346\240\267\345\274\217.md" @@ -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, diff --git "a/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" "b/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" index b02886b..c40e2a3 100644 --- "a/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" +++ "b/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" @@ -84,13 +84,14 @@ PDF中没有语义上的表格的概念,所以需要根据外观上的表格 ## 数据结构 -参考文本和图片,定义表格类型的数据结构: +表格块`TableBlock`继承了第一类基本数据`Block`的结构(详见[PDF提取](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)),同时新增了表征表格结构的`Row`和`Cell`。其中`Cell`也是布局级别的对象,嵌套了下一级的`Block`和`Shape`。 + ```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), @@ -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 diff --git "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" similarity index 66% rename from "docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" rename to "docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" index 14496f8..31e6276 100644 --- "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" +++ "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" @@ -3,24 +3,19 @@ categories: [process automation] tags: [python] --- -# pdf2docx开发概要:解析页面布局 +# pdf2docx开发概要:解析段落 --- -经过[表格解析](2020-08-15-pdf2docx开发概要:解析表格.md)、[文本样式识别](2020-07-20-pdf2docx开发概要:解析文本样式.md)后,我们得到了整合后的文本/图片/表格块。其中文本/图片块将被重建为段落,表格块将被重建为表格,单元格内文本/图片块按照相同的逻辑处理。在此基础上,计算相邻元素之间的间距,例如竖直方向的前后段间距、水平方向的段落缩进。 +经过[表格解析](2020-08-15-pdf2docx开发概要:解析表格.md)后,我们得到了整合的文本/图片/表格`Block`。其中文本/图片块将被重建为段落,表格块将被重建为表格,单元格内文本/图片块按照相同的逻辑处理。在此基础上,计算相邻元素之间的间距,例如竖直方向的前后段间距、水平方向的段落缩进。 -## 页面大小与页边距 - -`PyMuPDF`解析结果直接包括了页面宽度和高度,同时根据所有块级元素占据区域的极限值得到页边距,例如最小左上角点确定了左边距和上边距。 - -`python-docx`中页面`section`对象恰好提供了这六个属性,例如`page_width`、`left_margin`。于是页面基本形式得以确定。 ## 竖直方向定位:段间距 -块级元素之间通过段前/段后间距确定相对位置关系。由于Word中段落具有段前/段后间距属性,文本块将被作为定位的参考元素。 +`Block`块级元素之间通过间距确定相对位置关系。由于Word中 **段落具有段前/段后间距属性**,文本块将被作为定位的参考元素。 竖直间距确定原则: @@ -42,12 +37,15 @@ tags: [python] 已知块的高度和内部行数,容易计算得到平均行距。Word中有两种设置行间距的方式: -- 固定值:即为计算得到的平均行距,优点是定位精确,缺点是不会随着字体大小修改而改变,不利于编辑。 -- 倍数行距:与单倍行距的比值,优缺点与固定值行距正好相反 +- 固定值:直接设置为计算出的平均行距,优点是定位精确,缺点是不会随着字号的变化而改变,不利于编辑。例如一旦增大字号,则有可能导致显示不全。 + +- 倍数行距:与单倍行距的比值,优缺点与固定值行距正好相反。 + +综合来看,倍数行距有更好的适应性,v0.5.2版开始启用倍数行距。 -综合来看,倍数行距有更好的适应性,但是 **这个值的计算和字体相关**,单开一篇进行具体介绍: +注意,倍数行距并非与字号的简单比值或者流传的1.2倍的比例关系,而是 **与具体字体相关**。单开一篇介绍倍数行距计算问题: -> [TODO](to_do) +> [此坑待填...](to_do) ## 水平方向定位:对齐方式与缩进 @@ -85,15 +83,13 @@ tags: [python] ## 数据结构 -综上,文本/表格块在标准数据结构的基础上,加入了如下定位相关属性: +综上,文本块`TextBlock`在标准`Block`数据结构(`Line`->`Span`->`Char`)的基础上,加入了如下定位相关属性: ```python -# text/table block +# text block { "type": 0, "bbox": [float, float, float, float], - - ..., # text/table properties # ----- vertical spacing ----- "before_space": float, @@ -104,7 +100,32 @@ tags: [python] "alignment": int, "left_space": float, "right_space": float, - "tab_stops": [float, 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 + }] + } + ] + } + ] } ``` diff --git "a/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" "b/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" new file mode 100644 index 0000000..43f7361 --- /dev/null +++ "b/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" @@ -0,0 +1,162 @@ +--- +categories: [process automation] +tags: [python] +--- + +# pdf2docx开发概要:解析页面布局 + + +--- + +[前文](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)介绍了从PDF直接提取出的基本数据文本/图片/表格块`Block`和形状`Shape`,但是它们目前还只有位置数据,接下来要做的是解析语义数据,也就是从`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`,以保证页面逻辑的合理性,同时减少嵌套表格的泛滥。 + +!!! note "注意" + 兼顾通用性和降低复杂度,目前仅支持单栏或者两栏的布局,更多的分栏将被解析为表格。 + +### 分栏逻辑 + +自上而下检测每一行,将连续的单列或者双列归为同一个`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`。 + + +!!! warning "页面第一个`Section`的处理" + - 计算`before_space`时,前一个参考位置为页面上边距。 + - 重建`Section`时,新建一个空段落作为设置段后间距的参考。 + + +## 表格和段落解析 + + +创建`Section`后,将原始的`Block`和`Shape`按照位置关系分配到相应的`Column`中去,以便进一步解析表格和段落。`Column`是一个布局对象,直接容纳`Block`和`Shape`,所以`pdf2docx`为其抽象出一个`Layout`类,便于管理和操作。 + +``` +┌────────────────────────────────┐ +│Layout │ +│ ┌────────────┐ ┌───────────┐ │ +│ │Blocks │ │Shapes │ │ +│ │ TextBlock │ │ Stroke │ │ +│ │ TableBlock│ │ Fill │ │ +│ │ ... │ │ ... │ │ +│ └────────────┘ └───────────┘ │ +└────────────────────────────────┘ +``` + +解析出`TableBlock`后,继续根据位置关系将`Block`和`Shape`分配到相应的单元格中去。由此可知,容纳`Block`和`Shape`的单元格也是一个`Layout`对象,可以继续进行单元格内的嵌套表格和段落解析。这样的设计确保可以递归解析出无限嵌套的表格,同时也有助于将PDF的浮动布局转化为流动布局,便于在Word中重建。 + +至于表格的具体解析方法,单开一篇进行具体介绍: + +> [pdf2docx开发概要:解析表格](2020-08-15-pdf2docx开发概要:解析表格.md) + + + +最后,总结页面布局级别的数据结构及基本属性如下: + +```python +# 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 + } + ] +} +``` + From 3b1d69b88b1a0aa99b5ee3e8257233ec156fc4c1 Mon Sep 17 00:00:00 2001 From: dothinking Date: Tue, 1 Jun 2021 23:23:52 +0800 Subject: [PATCH 3/3] update posts: connection between posts --- ...07\345\222\214\345\275\242\347\212\266.md" | 10 ++++----- ...43\346\236\220\350\241\250\346\240\274.md" | 2 +- ...43\346\236\220\346\256\265\350\220\275.md" | 22 +++++++++++++------ ...65\351\235\242\345\270\203\345\261\200.md" | 18 +++++++-------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git "a/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" "b/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" index 1b047f2..5a96bb5 100644 --- "a/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" +++ "b/docs/2020-07-14-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\346\217\220\345\217\226\346\226\207\346\234\254\343\200\201\345\233\276\347\211\207\345\222\214\345\275\242\347\212\266.md" @@ -21,9 +21,9 @@ tags: [python] - 将图片块整合到文本块`TextBlock`中,作为`line`>`span`级别的元素 -- `block`增加了布局参数,例如`alignment`,`left_space`,`before_space`,用以保存后续段落布局解析结果 +- 增加布局参数,例如`alignment`,`left_space`,`before_space`,用以保存后续段落布局解析结果 + -- 新增了表格块`TableBlock`,用以保存表格解析结果 !!! warning "注意" 后续版本中发现`extractDICT()`获取图片存在问题(如只能获取完全显示在页面中的图片、丢失alpha通道),还需配合`page.getImageList()`处理;另外,v0.5.0版本中对浮动图片也进行了支持,详见以下两篇图片相关的记录。 @@ -154,11 +154,11 @@ EMC ## 总结 -借助`PyMuPDF`从PDF提取文本、图片和形状数据,它们构成了`pdf2docx`的基础数据: +借助`PyMuPDF`从PDF提取文本、图片和形状数据,它们构成了`pdf2docx`的两类基础数据: -- `Block`代表主体内容,例如文本/图片组成的`TextBlock`和表格结构的`TableBlock`,它们都将作为Word中的段落。 +- 块元素`Block`,代表主体内容,例如直接提取出的文本/图片组成的`TextBlock`,以及后续解析得到的表格结构`TableBlock`。 -- `Shape`代表格式内容,例如描边`Stroke`将对应下划线、表格边框等,填充`Fill`将对应文本高亮、单元格背景色等。 +- 形状元素`Shape`,代表格式内容,例如描边`Stroke`将对应下划线、表格边框等,填充`Fill`将对应文本高亮、单元格背景色等。 [^1]: [TextPage](https://pymupdf.readthedocs.io/en/latest/textpage.html) diff --git "a/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" "b/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" index c40e2a3..b1daa1a 100644 --- "a/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" +++ "b/docs/2020-08-15-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\350\241\250\346\240\274.md" @@ -84,7 +84,7 @@ PDF中没有语义上的表格的概念,所以需要根据外观上的表格 ## 数据结构 -表格块`TableBlock`继承了第一类基本数据`Block`的结构(详见[PDF提取](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)),同时新增了表征表格结构的`Row`和`Cell`。其中`Cell`也是布局级别的对象,嵌套了下一级的`Block`和`Shape`。 +表格块`TableBlock`继承了块元素`Block`的结构(详见[PDF提取](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)),同时新增了表征表格结构的`Row`和`Cell`。其中`Cell`是布局级别的对象(`Layout`),嵌套了下一级的块元素和形状元素,如此递归,直到块元素中不再包含表格,即只有表征文本和图片的`TextBlock`。 ```python diff --git "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" index 31e6276..7ce4324 100644 --- "a/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" +++ "b/docs/2020-08-27-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\346\256\265\350\220\275.md" @@ -9,13 +9,13 @@ tags: [python] --- -经过[表格解析](2020-08-15-pdf2docx开发概要:解析表格.md)后,我们得到了整合的文本/图片/表格`Block`。其中文本/图片块将被重建为段落,表格块将被重建为表格,单元格内文本/图片块按照相同的逻辑处理。在此基础上,计算相邻元素之间的间距,例如竖直方向的前后段间距、水平方向的段落缩进。 +经过[表格解析](2020-08-15-pdf2docx开发概要:解析表格.md)后,我们得到了整合的块元素:文本/图片块`TextBlock`和表格块`TableBlock`。其中文本/图片块将被重建为段落,表格块将被重建为表格,单元格内的块元素按照相同的逻辑进行递归处理。在此基础上,计算相邻元素之间的间距,例如竖直方向的前后段间距、水平方向的段落缩进。 ## 竖直方向定位:段间距 -`Block`块级元素之间通过间距确定相对位置关系。由于Word中 **段落具有段前/段后间距属性**,文本块将被作为定位的参考元素。 +块级元素之间通过间距确定相对位置关系。由于Word中 **段落具有段前/段后间距属性**,文本块将被作为定位的参考元素。 竖直间距确定原则: @@ -37,20 +37,20 @@ tags: [python] 已知块的高度和内部行数,容易计算得到平均行距。Word中有两种设置行间距的方式: -- 固定值:直接设置为计算出的平均行距,优点是定位精确,缺点是不会随着字号的变化而改变,不利于编辑。例如一旦增大字号,则有可能导致显示不全。 +- 固定值:直接设置为计算出的平均行距,优点是定位精确,缺点是不会随着字号的变化而改变,不利于编辑。例如一旦增大字号,则有可能导致该行文本显示不全。 -- 倍数行距:与单倍行距的比值,优缺点与固定值行距正好相反。 +- 倍数行距:与单倍行距的比值,优缺点刚好与固定行距相反。 综合来看,倍数行距有更好的适应性,v0.5.2版开始启用倍数行距。 -注意,倍数行距并非与字号的简单比值或者流传的1.2倍的比例关系,而是 **与具体字体相关**。单开一篇介绍倍数行距计算问题: +注意,倍数行距并非行高与字号的简单比值或者流传的1.2倍的比例关系,而是 **与具体字体相关**。单开一篇介绍倍数行距计算问题: > [此坑待填...](to_do) ## 水平方向定位:对齐方式与缩进 -水平方向从内部(块内元素的对齐关系)和外部(页面中的位置)两个方面确定对齐方式:左/居中/右/分散对齐。其中左对齐为默认方式,因为结合段落左缩进和制表符,总能正确定位任何块间元素。 +水平方向从内部(块内元素`Line`的对齐关系)和外部(页面中的位置)两个方面确定对齐方式:左/居中/右/分散对齐。其中左对齐为默认方式,因为结合段落左缩进和制表符,总能正确定位任何块间元素。 **内部对齐关系**:行与行之间位置关系 @@ -81,9 +81,17 @@ tags: [python] - 如果只有一行,则将这个放宽放到极限,即设置相应边距等于0。 +## 文本样式 + +以上解析结果确定了段落在页面中的位置和呈现样式,接下来深入到段内文本。`PyMuPDF`提取的原始文本块自带了字体、颜色、斜体、粗体等属性,但是`高亮`、`下划线`、`删除线`等具体样式需要进一步根据 **文本和形状的位置关系** 来判定。 + +具体参考下文: + +> [pdf2docx开发概要:解析文本样式](2020-07-20-pdf2docx开发概要:解析文本样式.md) + ## 数据结构 -综上,文本块`TextBlock`在标准`Block`数据结构(`Line`->`Span`->`Char`)的基础上,加入了如下定位相关属性: +综上,文本块`TextBlock`在标准`Block`数据结构(`Line`->`Span`->`Char`)的基础上,引入了如下定位相关属性: ```python # text block diff --git "a/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" "b/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" index 43f7361..f217b74 100644 --- "a/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" +++ "b/docs/2021-05-30-pdf2docx\345\274\200\345\217\221\346\246\202\350\246\201\357\274\232\350\247\243\346\236\220\351\241\265\351\235\242\345\270\203\345\261\200.md" @@ -8,7 +8,7 @@ tags: [python] --- -[前文](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)介绍了从PDF直接提取出的基本数据文本/图片/表格块`Block`和形状`Shape`,但是它们目前还只有位置数据,接下来要做的是解析语义数据,也就是从`Block`解析出段落和段落属性如段间距、对齐方式,从`Shape`解析出文本样式例如下划线、高亮及其作用的文本、解析表格样式如边框颜色、背景色及其包含的段落。我们先从页面布局开始。 +[前文](2020-07-14-pdf2docx开发概要:提取文本、图片和形状.md)介绍了从PDF直接提取出的基本数据:块元素`Block`和形状`Shape`,但是它们目前还只有位置数据,接下来要做的是解析语义数据,例如从块元素解析出表格和普通段落,从形状元素解析出文本样式例如下划线、高亮及其作用的文本、解析表格样式如边框颜色、背景色及其包含的段落。我们先从页面布局开始。 ## 数据结构 @@ -17,9 +17,9 @@ tags: [python] - `Page`类对应物理上的一个页面,具有尺寸、边距、页眉页脚等属性,主要内容按照流式布局顺序被分为一个或者多个`Section` -- `Section`对应Word中的Section,表示具有类似结构的布局,例如一个或者多个分栏`Column` +- `Section`对应Word的节(Section),表示具有类似结构的布局,例如一个或者多个分栏`Column` -- `Column`对应Word中的Column,表示分栏中的一列。对于普通的排版,主要元素都在一个`Section`的一个`Column`下 +- `Column`对应Word的分栏(Column),表示分栏中的一列。对于普通的排版,块元素和形状元素都在一个`Section`的一个`Column`下 ``` ┌───────────────────────────────────┐ @@ -42,14 +42,14 @@ tags: [python] ## 页眉页脚 -理论上,无法从单个页面识别出页眉和页脚,所以需要从文档级别、多个Pages一起对比结构相似性,从而从页面开始部分分离出页眉、从页面结束部分分离出页脚。同时,需要处理一些不确定因素,例如页码、奇偶页不同的页眉页脚。 +理论上,无法从单个页面识别出页眉和页脚,所以需要从文档级别即多个Pages一起对比结构相似性,从而从页面开始部分分离出页眉、从页面结束部分分离出页脚。同时,需要处理一些不确定因素,例如页码、奇偶页不同的页眉页脚。 截至`v0.5.2`,这部分依然占坑待处理中。 ## 页面大小与页边距 -`PyMuPDF`提取的数据直接包括了页面宽度和高度,然后根据 **除去页眉页脚后** 的所有块级元素所占据的区域计算页边距,例如最小左上角点确定了左边距和上边距。 +`PyMuPDF`提取的数据直接包括了页面宽度和高度,然后根据 **除去页眉页脚后** 的所有块级、形状元素所占据的区域计算页边距,例如最小左上角点确定了左边距和上边距。 `python-docx`中页面`section`对象恰好提供了这六个属性,例如`page_width`、`left_margin`。于是页面基本形式得以确定。 @@ -59,7 +59,7 @@ tags: [python] 早期版本通过表格来实现分栏布局,从0.5.2版开始引入`Section`和`Column`,以保证页面逻辑的合理性,同时减少嵌套表格的泛滥。 !!! note "注意" - 兼顾通用性和降低复杂度,目前仅支持单栏或者两栏的布局,更多的分栏将被解析为表格。 + 为兼顾通用性和降低复杂度,目前仅支持单栏或者两栏的布局,更多的分栏将被解析为表格。 ### 分栏逻辑 @@ -82,10 +82,10 @@ tags: [python] 每一个`Section`在页面竖直方向的位置 **由前一个段落的段后间距** 确定,因此在确定好`Section`后,计算当前`Section`开始位置`y0`与前一个`Section`结束位置`y1`的差值,作为`Section`的一个属性`before_space`。 -使用`python-docx`重建`Section`时,设置前一个`Section`最后一个段落的段后间距等于当前`Section`的`before_space`即可。两个`Column`之间设置列分隔符`WD_SECTION.NEW_COLUMN`。 +使用`python-docx`重建`Section`时,设置前一个`Section`最后一个段落的段后距离等于当前`Section`的`before_space`即可。两个`Column`之间设置列分隔符`WD_SECTION.NEW_COLUMN`。 -!!! warning "页面第一个`Section`的处理" +!!! warning "每一页第一个`Section`的处理" - 计算`before_space`时,前一个参考位置为页面上边距。 - 重建`Section`时,新建一个空段落作为设置段后间距的参考。 @@ -107,7 +107,7 @@ tags: [python] └────────────────────────────────┘ ``` -解析出`TableBlock`后,继续根据位置关系将`Block`和`Shape`分配到相应的单元格中去。由此可知,容纳`Block`和`Shape`的单元格也是一个`Layout`对象,可以继续进行单元格内的嵌套表格和段落解析。这样的设计确保可以递归解析出无限嵌套的表格,同时也有助于将PDF的浮动布局转化为流动布局,便于在Word中重建。 +PDF直接提取出的块元素只有`TextBlock`,结合形状元素(潜在的表格边框、单元格背景色)解析出`TableBlock`后,继续根据位置关系将表格范围内的`Block`和`Shape`分配到相应的单元格中去。由此可知,容纳`Block`和`Shape`的单元格也是一个`Layout`对象,可以继续进行单元格内的嵌套表格和段落解析。这样的设计确保可以递归解析出无限嵌套的表格,同时也有助于将PDF的浮动布局转化为流动布局,便于在Word中重建。 至于表格的具体解析方法,单开一篇进行具体介绍: