附录 3:各种技术信息#

本节涉及各种技术主题,这些主题不一定相互相关。


图像变换矩阵#

从版本 1.18.11 开始,一些文本和图像提取的方法返回图像变换矩阵: Page.get_text()Page.get_image_bbox()

变换矩阵包含关于图像如何被转化以适应某个文档页面上的矩形(其“边界框” = “bbox”)的信息。通过检查页面上的图像的 bbox 和该矩阵,可以确定例如图像在页面上的显示是否被缩放或旋转,以及如何显示。

图像尺寸与页面上其边界框之间的关系如下:

  1. Using the original image’s width and height,
    • 定义图像矩形 imgrect = pymupdf.Rect(0, 0, width, height)

    • 定义“收缩矩阵” shrink = pymupdf.Matrix(1/width, 0, 0, 1/height, 0, 0).

  2. 使用其缩小矩阵转换图像矩形,将得到单位矩形: imgrect * shrink = pymupdf.Rect(0, 0, 1, 1).

  3. 使用图像 变换矩阵 “transform”,以下步骤将计算边界框:

    imgrect = pymupdf.Rect(0, 0, width, height)
    shrink = pymupdf.Matrix(1/width, 0, 0, 1/height, 0, 0)
    bbox = imgrect * shrink * transform
    
  4. 检查矩阵乘积 shrink * transform 将揭示关于图像矩形为适应页面上的 bbox 而发生的所有信息:旋转、边的缩放和原点的平移。让我们看一个例子:

    >>> imginfo = page.get_images()[0]  # get an image item on a page
    >>> imginfo
    (5, 0, 439, 501, 8, 'DeviceRGB', '', 'fzImg0', 'DCTDecode')
    >>> #------------------------------------------------
    >>> # define image shrink matrix and rectangle
    >>> #------------------------------------------------
    >>> shrink = pymupdf.Matrix(1 / 439, 0, 0, 1 / 501, 0, 0)
    >>> imgrect = pymupdf.Rect(0, 0, 439, 501)
    >>> #------------------------------------------------
    >>> # determine image bbox and transformation matrix:
    >>> #------------------------------------------------
    >>> bbox, transform = page.get_image_bbox("fzImg0", transform=True)
    >>> #------------------------------------------------
    >>> # confirm equality - permitting rounding errors
    >>> #------------------------------------------------
    >>> bbox
    Rect(100.0, 112.37525939941406, 300.0, 287.624755859375)
    >>> imgrect * shrink * transform
    Rect(100.0, 112.375244140625, 300.0, 287.6247253417969)
    >>> #------------------------------------------------
    >>> shrink * transform
    Matrix(0.0, -0.39920157194137573, 0.3992016017436981, 0.0, 100.0, 287.6247253417969)
    >>> #------------------------------------------------
    >>> # the above shows:
    >>> # image sides are scaled by same factor ~0.4,
    >>> # and the image is rotated by 90 degrees clockwise
    >>> # compare this with pymupdf.Matrix(-90) * 0.4
    >>> #------------------------------------------------
    

PDF 基础 14 种字体#

以下14个内置字体名称必须被每个PDF查看器应用程序支持。它们可用作字典,字典将全名和小写的缩写映射到完整的字体基名。在PyMuPDF中,任何情况下需要提供字体名称时,可以使用字典中的键或值

In [2]: pymupdf.Base14_fontdict
Out[2]:
{'courier': 'Courier',
'courier-oblique': 'Courier-Oblique',
'courier-bold': 'Courier-Bold',
'courier-boldoblique': 'Courier-BoldOblique',
'helvetica': 'Helvetica',
'helvetica-oblique': 'Helvetica-Oblique',
'helvetica-bold': 'Helvetica-Bold',
'helvetica-boldoblique': 'Helvetica-BoldOblique',
'times-roman': 'Times-Roman',
'times-italic': 'Times-Italic',
'times-bold': 'Times-Bold',
'times-bolditalic': 'Times-BoldItalic',
'symbol': 'Symbol',
'zapfdingbats': 'ZapfDingbats',
'helv': 'Helvetica',
'heit': 'Helvetica-Oblique',
'hebo': 'Helvetica-Bold',
'hebi': 'Helvetica-BoldOblique',
'cour': 'Courier',
'coit': 'Courier-Oblique',
'cobo': 'Courier-Bold',
'cobi': 'Courier-BoldOblique',
'tiro': 'Times-Roman',
'tibo': 'Times-Bold',
'tiit': 'Times-Italic',
'tibi': 'Times-BoldItalic',
'symb': 'Symbol',
'zadb': 'ZapfDingbats'}

与他们的义务相对,并非所有PDF查看器都可以正确而完整地支持这些字体——这尤其适用于Symbol和ZapfDingbats。此外,字形(视觉)图像将对每个阅读器都是特定的。

要了解这些字体如何使用——包括CJK 内置字体——请查看Page.insert_font()中的表格。


Adobe PDF 参考#

由Adobe发布的此PDF参考手册在本文件中经常被引用。可以从这里查看和下载。

注意

很长一段时间,旧版本也可以通过 this 链接访问。它似乎在2021年10月被从网站上移除了。早期(1.19.* 之前)版本的 PyMuPDF 文档曾提到过这个文件。我们已经开始努力替换对上述当前规范的引用。


在 PyMuPDF 中使用 Python 序列作为参数#

当PyMuPDF对象和方法需要一个Python list 数值列表时,其他Python sequence types 也是允许的。Python类被认为实现了sequence protocol,如果它们具有一个__getitem__()方法。

这基本上意味着,在这些情况下,您可以交替使用 Python listtuple 甚至 array.arraynumpy.arraybytearray 类型。

例如,以以下任意方式指定一个序列 "s"

  • s = [1, 2] – 一个列表

  • s = (1, 2) – 一个元组

  • s = array.array("i", (1, 2)) – 一个 array.array

  • s = numpy.array((1, 2)) – 一个 numpy 数组

  • s = bytearray((1, 2)) – 一个字节数组

将在以下示例表达式中使其可用:

  • pymupdf.Point(s)

  • pymupdf.Point(x, y) + s

  • doc.select(s)

同样适用于所有几何对象 Rect, IRect, MatrixPoint

因为所有的 PyMuPDF 几何类本身都是序列的特殊情况,它们(除了 Quad - 见下文)可以在可以使用数值序列的地方自由使用,例如作为 list()tuple()array.array()numpy.array() 函数的参数。查看以下代码片段以了解其工作原理。

>>> import pymupdf, array, numpy as np
>>> m = pymupdf.Matrix(1, 2, 3, 4, 5, 6)
>>>
>>> list(m)
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
>>>
>>> tuple(m)
(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
>>>
>>> array.array("f", m)
array('f', [1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
>>>
>>> np.array(m)
array([1., 2., 3., 4., 5., 6.])

注意

Quad是一个Python序列对象,长度为4。然而,它的项目是point_like - 而不是数字。因此,上述评论不适用。


确保PyMuPDF中重要对象的一致性#

PyMuPDF 是 C 库 MuPDF 的 Python 绑定。虽然 MuPDF 的创造者们投入了大量精力来近似某种面向对象的行为,但他们当然无法克服 C 语言在这方面的基本缺陷。

另一方面,Python以非常简洁的方式实现了面向对象模型。PyMuPDF和MuPDF之间的接口代码由两个基本文件组成:pymupdf.pyfitz_wrap.c。它们是由出色的SWIG工具为每个新版本创建的。

当您使用 PyMuPDF 的对象或方法时,这将导致在 pymupdf.py 中执行一些代码,而这反过来将调用一些用 fitz_wrap.c 编译的 C 代码。

因为SWIG在保持Python和C级别同步方面做了很多工作,只要严格遵循一系列规则,一切都能正常工作。例如:切勿访问一个Page对象,在关闭(或删除或设置为None)拥有的Document后。或者,更不明显的是:切勿访问一个页面或它的任何子元素(链接或注释),在你执行了文档方法select()delete_page()insert_page() …等之后。

但是仅仅不再访问无效对象实际上是不够的:它们应该被主动完全删除,以释放C级资源(意味着分配的内存)。

这些规则的原因在于文档及其页面之间以及页面及其链接/注释之间存在二级多对一关系。为了保持一致的情况,以上任何操作都必须导致完全重置——在 Python 和 C 中同步进行

SWIG 无法知道这一点,因此不执行此操作。

因此,所需的逻辑已以以下方式内置于PyMuPDF中。

  1. 如果一个页面“失去”其拥有的文档或正在被删除,则它所有当前存在的注释和链接将在Python中变得不可用,并且它们的C级对应项将被删除和释放。

  2. 如果文档被关闭(或删除或设置为 None)或其结构发生变化,则所有当前存在的页面及其子页面也将变得不可用,并将进行相应的C级删除。“结构变化”包括像 select()delePage()insert_page()insert_pdf() 等方法:所有这些都将导致对象删除的级联。

程序员通常不会意识到这一点。然而,如果他尝试访问无效的对象,将会引发异常。

无效的对象无法像Python语句一样直接删除,例如del pagepage = None等。相反,必须调用它们的__del__方法。

所有页面、链接和注释都有属性 parent,指向拥有对象。这是可以在应用级别检查的属性:如果 obj.parent == None,则对象的父对象已消失,对其属性或方法的任何引用将引发异常,告知此“孤立”状态。

一个示例会话:

>>> page = doc[n]
>>> annot = page.first_annot
>>> annot.type                    # everything works fine
[5, 'Circle']
>>> page = None                   # this turns 'annot' into an orphan
>>> annot.type
<... omitted lines ...>
RuntimeError: orphaned object: parent is None
>>>
>>> # same happens, if you do this:
>>> annot = doc[n].first_annot     # deletes the page again immediately!
>>> annot.type                    # so, 'annot' is 'born' orphaned
<... omitted lines ...>
RuntimeError: orphaned object: parent is None

这显示了级联效果:

>>> doc = pymupdf.open("some.pdf")
>>> page = doc[n]
>>> annot = page.first_annot
>>> page.rect
pymupdf.Rect(0.0, 0.0, 595.0, 842.0)
>>> annot.type
[5, 'Circle']
>>> del doc                       # or doc = None or doc.close()
>>> page.rect
<... omitted lines ...>
RuntimeError: orphaned object: parent is None
>>> annot.type
<... omitted lines ...>
RuntimeError: orphaned object: parent is None

注意

上述关系之外的对象不包括在该机制中。比如,如果您通过 toc = doc.get_toc() 创建了一个目录,随后关闭或更改文档,那么这些操作不会以任何方式更改变量 toc。刷新这些变量的责任在您。


方法设计 Page.show_pdf_page()#

目的与能力#

该方法在当前(“包含”,“目标”)页面的指定矩形内显示另一个PDF文档的(“源”)页面的图像。

当前支持以下显示的变体:

  • Bool parameter "keep_proportion" controls whether to maintain the aspect ratio (default) or not.
    • 矩形参数 "clip" 限制源页面矩形的可见部分。默认是整个页面。

  • float "rotation" 将显示旋转一个任意角度(度)。如果这个角度不是90的整数倍,只有4个角中的2个可以定位在目标边界上,如果"keep_proportion"也为真。

  • 布尔参数 "overlay" 控制是否将图像放置在当前页面内容的顶部(前景,默认)还是放置在底部(背景)。

用例包括(但不限于)以下内容:

  1. 用相同的图像(例如公司标志或水印)“盖印”当前文档的一系列页面。

  2. 将任意输入页面合并为一个输出页面,以支持“手册”或双面打印(称为“4-up”,“n-up”)。

  3. 将(大)输入页面拆分为多个任意部分。这也称为“海报化”,因为您可以横向和纵向拆分A4页面,打印放大后的4个部分到单独的A4页面,从而最终得到您原始页面的A2版本。

技术实现#

这是通过 PDF “Form XObjects” 完成的,详见 Adobe PDF References 第 217 页的第 8.10 节。当执行 Page.show_pdf_page() 时,会发生以下事情:

  1. 源文档中源页面的 resourcescontents 对象被复制到目标文档中,共同创建一个新的 Form XObject,具有以下属性。该对象的 PDF xref 编号由该方法返回。

    1. /BBox 等于 /Mediabox 的源页面

    2. /Matrix 等于单位矩阵。

    3. /Resources 等于源页面的内容。这涉及到对层次嵌套的其他对象(包括字体、图片等)的“深拷贝”。这里涉及的复杂性由 MuPDF 的嫁接 [1] 技术函数涵盖。

    4. 这是一个流对象类型,它的流是源页面contents对象组合数据的精确副本。

    此表单XObject仅在每个显示的源页面上执行一次。后续显示相同的源页面将跳过此步骤,仅创建指向此对象的“指针”表单XObjects(在下一个步骤中完成)。

  2. 然后创建第二个 Form XObject,目标页面使用它来调用显示。该对象具有以下属性:

    1. /BBox 等于源页面的 /CropBox(或 "clip")。

    2. /Matrix 表示 /BBox 到目标矩形的映射。

    3. /XObject 通过固定名称 fullpage 引用前一个 Form XObject。

    4. 这个对象的流包含一个固定的语句: /fullpage Do

    5. 如果方法的 "oc" 参数被提供,它的值将被分配给这个 Form XObject 为 /OC

  3. 目标页面的 resourcescontents 对象现在修改如下。

    1. /Resources/XObject字典添加一个条目,名称为fzFrm(选择n以确保此条目在页面上是唯一的)。

    2. 根据 "overlay",在页面的 /Contents 数组中添加一个新对象,包含语句 q /fzFrm Do Q

这种设计方法确保:

  1. 源页面(可能很大)仅复制一次到目标PDF。每个目标页面仅创建小的“指针”表单XObjects对象以显示源页面。

  2. 每个引用目标页面可以有自己的 "oc" 参数,以单独控制源页面的可见性。

诊断#

PyMuPDF 消息#

PyMuPDF 具有用于显示文本诊断的消息系统。

默认情况下,消息会写入sys.stdout。这可以通过两种方式进行控制:

MuPDF 错误和警告#

MuPDF 生成文本错误和警告。

一些MuPDF错误可能导致Python异常。

可恢复错误的示例输出。我们正在打开一个损坏的PDF,但MuPDF能够修复它,并给我们一些关于发生了什么的小信息。然后我们说明如何找出文档是否可以后续增量保存。在这一点上检查Document.is_dirty属性也表明在pymupdf.open期间文档必须被修复:

>>> import pymupdf
>>> doc = pymupdf.open("damaged-file.pdf")  # leads to a sys.stderr message:
mupdf: cannot find startxref
>>> print(pymupdf.TOOLS.mupdf_warnings())  # check if there is more info:
cannot find startxref
trying to repair broken xref
repairing PDF document
object missing 'endobj' token
>>> doc.can_save_incrementally()  # this is to be expected:
False
>>> # the following indicates whether there are updates so far
>>> # this is the case because of the repair actions:
>>> doc.is_dirty
True
>>> # the document has nevertheless been created:
>>> doc
pymupdf.Document('damaged-file.pdf')
>>> # we now know that any save must occur to a new file

无法恢复的错误的示例输出:

>>> import pymupdf
>>> doc = pymupdf.open("does-not-exist.pdf")
mupdf: cannot open does-not-exist.pdf: No such file or directory
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    doc = pymupdf.open("does-not-exist.pdf")
  File "C:\Users\Jorj\AppData\Local\Programs\Python\Python37\lib\site-packages\fitz\pymupdf.py", line 2200, in __init__
    _pymupdf.Document_swiginit(self, _pymupdf.new_Document(filename, stream, filetype, rect, width, height, fontsize))
RuntimeError: cannot open does-not-exist.pdf: No such file or directory
>>>

坐标#

这是本文件中最常用的术语之一。坐标通常指一对数字 (x, y),表示某个位置,比如矩形的一个角落 (Rect)、一个 Point 等等。这两个值通常是浮点数,但有些对象如图像只允许它们为整数。

要真正找到一个坐标的位置,我们还需要知道参考点于xy - 换句话说,我们必须知道位置(0, 0)的位置。一旦(0, 0)("原点")已知,我们就谈论“坐标系”。

在文档处理过程中存在几种坐标系统。例如,PDF页面的坐标系统与由此创建的图像的坐标系统是不同的。因此,我们需要某种方法来转换坐标,从一个系统转换到另一个系统(有时也可以再转换回去)。这就是一个矩阵的任务。它是一个数学函数,工作原理类似于一个可以与点或矩形“相乘”的因子,从而给我们在另一个坐标系统中的相应点/矩形。变换矩阵的逆可以用来恢复变换。就像用某个因子,比如说3来相乘,可以通过将结果除以3(或乘以1/3)来恢复。

坐标和图像#

图像有一个带有整数坐标的坐标系统。原点 (0, 0) 是左上角的点。x 值必须在 range(width) 内,y 值必须在 range(height) 内。因此,y增加 时我们往 下方 移动。对于每个图像,只有一个 有限的数量 的坐标, namely width * height。图像中的一个位置也称为“像素”。

  • 图像在打印时会有多(以厘米或英寸为单位),取决于额外的信息:“分辨率”。这以DPI(每英寸点数或每英寸像素)来测量。因此,要找出某个图像的打印大小,我们必须将其宽度和高度分别除以相应的DPI值(宽度和高度可能有不同的值),就能得到相应的英寸数。

原点、点大小和Y轴#

PDF 中,页面的原点 (0, 0) 位于其 左下角。在 MuPDF 中,页面的原点 (0, 0) 位于其 左上角

_images/img-coordinate-space.png

坐标是浮点数,单位为 ,其中:

  • 一点等于 1/72 英寸.

典型的文档页面大小是 ISO A4Letter。一个 Letter 页面的大小为 8.5 x 11 英寸,对应于 612 x 792 点。在 PDF 坐标系统中,Letter 页面左上角的点因此具有坐标 (0, 792),因为 y 轴向上。现在我们知道文档的大小,MuPDF 坐标系统中右下角的坐标将为 (612, 792) (而对于 PDF ,这个坐标将是 (612,0))。

  • 理论上,一个PDF页面上有无数个坐标位置。然而在实际应用中,最多需要前5位小数以达到合理的精度。

  • MuPDF 中,支持多种文档格式 - PDF 仅仅是 十多种其他格式 之一。图像也被支持作为 MuPDF 中的文档(因此通常只有一页)。这就是 MuPDF 使用坐标系统的原因之一,原点 (0, 0) 是任何文档页面的 左上角y轴向下,与图像相同。在 MuPDF 中,坐标无论如何都是浮点数,像在 PDF 中一样。

  • 例如,在 MuPDF(因此也在 PyMuPDF)中,一个矩形 Rect(0, 0, 100, 100) 是一个边长为 100 个点(= 1.39 英寸或 3.53 厘米)的正方形。它的左上角是原点。要在PDFMuPDF之间切换, 每个 Page 对象都有一个 Page.transformation_matrix。其逆可以用来计算矩形的 PDF 坐标。通过这种方式,我们可以方便地找到 Rect(0, 0, 100, 100)MuPDFRect(0, 692, 100, 792)PDF 是相同的。请参见以下代码片段:

    >>> page = doc.new_page(width=612, height=792)  # make new Letter page
    >>> ptm = page.transformation_matrix
    >>> # the inverse matrix of ptm is ~ptm
    >>> pymupdf.Rect(0, 0, 100, 100) * ~ptm
    Rect(0.0, 692.0, 100.0, 792.0)
    

脚注


本软件按原样提供,不作任何明示或暗示的担保。该软件根据许可证分发,除非按照该许可证的条款明确授权,否则不得复制、修改或分发。有关许可信息,请参阅artifex.com或联系Artifex Software Inc.,地址:39 Mesa Street, Suite 108A, San Francisco CA 94129, United States以获取更多信息。