pdf2docx开发概要:获取图片及其位置¶
发布于:2020-10-15 | 分类:process automation
本文初稿基于 PyMuPDF v1.18.0 版本撰写,后续版本更新,部分问题得以改善/解决。参见文中具体注释。
为了在docx中重建图片,我们需要提取图片的内容(二进制流)及其位置(坐标)。PyMuPDF
提供了两个获取页面图片的API:page.getText('rawdict')
和page.getImageList()
,但是截至v1.18.0
版本,其中任意单独一个方法都无法完成这个任务,本文记录综合这两个API,提取PDF页面图片及其位置的方法。
page.getText('rawdict')
与 page.getImageList()
¶
以v1.18.0
版本为例,这两个API(下文分别以rawdict
和getImageList
指代)的使用说明参考官方文档:
- https://pymupdf.readthedocs.io/en/latest/page.html#Page.getText
- https://pymupdf.readthedocs.io/en/latest/page.html#Page.getImageList
简要概括一下:
-
rawdict
返回当前页面内元素(文本、图片)字典,其中与图片相关的主要键及其意义如下key description bbox 图片区域:左上角、右下角坐标 (x0, y0, x1, y1) width, height 图片原始宽度和高度 ext 图片后缀名 image bytes
格式的图片内容 -
getImageList
返回当前页面内图片列表,其中每个元素是一个元组(xref, smask, width, height, bpc, colorspace, alt. colorspace, name, filter, referencer)
key description xref 图片内容索引号, fitz.Pixmap(doc, xref)
获取到位图内容smask 图片遮罩(Soft Mask)索引号,存储通道数据 width, height 图片原始宽度和高度
初步对比,二者相同点是都能获取到图片内容,不同点在于rawdict
可以直接获取到位置信息,而getImageList
得到了图片的原始索引xref
,可以获取包括图片位置(page.getImageBbox(item)
)在内的更多信息。
注意
page.getImageBbox(item)
获取的位置信息并非100%可靠 Issue-699
这样看来,rawdict
足以满足获取图片内容和位置的要求。但是,具体实践中却发现了一些特殊情形。
1. rawdict
或getImageList
获取图片的背景色错误¶
对于某些图片,无论是rawdict
的image
,还是getImageList
的fitz.Pixmap(doc, xref)
得到的图片颜色都不正确。具体又分为以下两种情形:
(a) CMYK
颜色空间或透明图片,参见Issue-670¶
PyMuPDF
官方示例借助pillow
库进行alpha
通道修复,本文采用了PyMuPDF
原生方法:
- 如果存在则加入
alpha
通道 - 如果不是常规的
RGB
或gray
颜色空间(例如CMYK
),则转换到RGB
空间
根据作者的解释,原生方法并非100%稳定;但是于我而言,可以避免仅仅因为较小概率的特殊情况而引入pillow
库依赖。
def recover_pixmap(doc:fitz.Document, item:list):
'''Restore pixmap with soft mask considered.
---
- doc: fitz document
- item: an image item got from page.getImageList()
'''
# data structure of `item`:
# (xref, smask, width, height, bpc, colorspace, ...)
x = item[0] # xref of PDF image
s = item[1] # xref of its /SMask
# base image
pix = fitz.Pixmap(doc, x)
# reconstruct the alpha channel with the smask if exists
if s > 0:
# copy of base image, with an alpha channel added
pix = fitz.Pixmap(pix, 1)
# create pixmap of the /SMask entry
ba = bytearray(fitz.Pixmap(doc, s).samples)
for i in range(len(ba)):
if ba[i] > 0: ba[i] = 255
pix.setAlpha(ba)
# we may need to adjust something for CMYK pixmaps here ->
# recreate pixmap in RGB color space if necessary
# NOTE: pix.colorspace may be None for images with alpha channel values only
if pix.colorspace and not pix.colorspace.name in (fitz.csGRAY.name, fitz.csRGB.name):
pix = fitz.Pixmap(fitz.csRGB, pix)
return pix
(b) 只有alpha
通道的图片,参见Issue-677¶
注意上面代码中,当颜色空间pix.colorspace
为空,也就是一副仅有alpha
通道的图片,那么上述为基本图片添加alpha
通道的方法将失效。
The problem here is that the image at xref 9 consists of alpha values only, i.e. colorspace None as you noted. The turquoise color you see in the PDF is not part of the image, but part of PDF background, established right before that image is invoked for display.
解决这个问题的一个取巧的方法是:获取该图片的区域,然后在该区域内截图。同时处理两个具体问题:
- 为避免重叠的文本出现在截图区域,需要在截图前隐藏文本
- 调整分辨率以保证截图的质量
def clip_page(page:fitz.Page, bbox:fitz.Rect=None, zoom:float=3.0):
'''Clip page pixmap (without text) according to `bbox` (entire page by default).
'''
# hide text before clip the image only
# render Tr: set the text rendering mode
# - 3: neither fill nor stroke the text -> invisible
# read more:
# - https://github.com/pymupdf/PyMuPDF/issues/257
# - https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
doc = page.parent
for xref in page._getContents():
stream = doc._getXrefStream(xref).replace(b'BT', b'BT 3 Tr') \
.replace(b'Tm', b'Tm 3 Tr') \
.replace(b'Td', b'Td 3 Tr')
doc._updateStream(xref, stream)
# improve resolution
# - https://pymupdf.readthedocs.io/en/latest/faq.html#how-to-increase-image-resolution
# - https://github.com/pymupdf/PyMuPDF/issues/181
bbox = page.rect if bbox is None else bbox & page.rect
image = page.getPixmap(clip=bbox, matrix=fitz.Matrix(zoom, zoom)) # type: fitz.Pixmap
return image
综上,我们需要一一处理getImageList
获取的图片(rawdict
无法给出alpha
通道的数据)
def extract_images(page:fitz.Page, clip_image_res_ratio:float=3.0):
''' Get images list with contents recovered.'''
doc = page.parent # pdf document
# check each image item:
# (xref, smask, width, height, bpc, colorspace, ...)
images = []
for item in page.getImageList(full=True):
try:
item = list(item)
item[-1] = 0
bbox = page.getImageBbox(item)
except ValueError:
continue
# ignore images outside page
if not bbox.intersects(page.rect): continue
# recover cmyk / transparent images
pix = recover_pixmap(doc, item)
# clip page for image with alpha values only
if not pix.colorspace:
pix = clip_page(page, bbox, zoom=clip_image_res_ratio)
images.append(pix)
return images
2. getImageList
不包含同一图片的不同实例¶
上面基于getImageList
的方法貌似完美了,但实际上,如果同一图片复制多次后粘贴在页面上,getImageList
仅仅包含其中一个实例,而rawdict
则会计算该图片出现的每个地方。
Image blocks in a textpage are generated for every image location – whether or not there are any duplicates. This is in contrast to Page.getImageList(), which will contain each image only once.
1.18.13 版本新增了get_image_rects
方法,它是getImageBbox
(即后续改用下划线方式的get_image_bbox
)的改进方法,可以获取同一图片的每一个引用实例的具体位置。 因此,不必再借助rawdict
获取所有实例,即以下自删除线段落起的(1)-(4)步骤已经过期。
The result is a list of Rect or (Rect, Matrix) objects – depending on transform. Each list item represents one location of the image on the page. Multiple occurrences might not be detectable by Page.get_image_bbox().
所以还不能放弃 rawdict。此时,我们已知:
getImageList
得到了图片的正确内容,以及重复图片中的某一个位置rawdict
得到了所有重复图片的正确位置
结合二者即可得到正确的内容和所有位置:
- (1) 以
rawdict
的image
值为键分组,每一组即为重复图片列表,列表中图片的位置正确,但是内容可能不正确(例如丢失alpha
通道) - (2) 如果
getImageList
某一图片的位置bbox
出现在上述某一分组的bbox
列表中,则将改组所有图片的内容修正为getImageList
该图片的内容
3. rawdict
仅包含完全显示在页面内的图片¶
上一步的处理以rawdict
为基础,以getImageList
进行修正。如果一副图片有任何部分出现在页面之外,则并不会被rawdict
统计。所以需要在上面处理逻辑中再加一步:
- (3) 如果
getImageList
某一图片的位置bbox
不出现在任何rawdict
分组中,则加入该图片
4. rawdict
可能包含“虚假”图片¶
上一步补救了getImageList
含有rawdict
中不存在的图片的情形;相反,rawdict
也可能含有getImageList
中不存在的图片,但实际上,这些是“虚假”图片——因为getImageList
统计了所有已显示或者未显示的图片。本文遇到的一个实例,某些复杂的矢量图形被错误地转换为图片而计算到rawdict
中去了。
此时,再追加一步:
- (4) 如果
getImageList
所有图片的位置bbox
都不出现在某一rawdict
分组,则删除该分组
5. PDF扫描图片的一些例外¶
自初稿以来,实践中又发现通常出现在PDF扫描件中的两类例外:
-
人眼可见的一张完整图片在PDF中却是由许多张子图组成的,虽然直接提取多幅子图逻辑上没错,但是通过相交检测并将其合并为一张图片输出,可以得到跟所见更为相符的结果。
-
当原始PDF坐标系与渲染后页面之间存在非零的旋转角度时,人眼可见的正立图片实际是以一定旋转角度存储在PDF中的,因此直接提取的图片是旋转的,需要借助图像处理库逆向旋转回去才能得到和所见一致的图片。
总结¶
getImageList
或rawdict
得到图片的内容可能不正确,需要进行修复或截图处理getImageList
获取的图片(显示/未显示)才算真正的图片rawdict
获取重复图片的每一个位置- 注意 图片分割 及 页面旋转 引起直接提取的图片和最终渲染结果之间的差异