PyMuPDF 1.24.4 中文文档(五)(3)https://developer.aliyun.com/article/1559438
如何访问 PDF 文件尾
PDF 文件的尾部是一个位于文件末尾的dictionary
。它包含特殊对象和指向重要其他信息的指针。请参见 Adobe PDF References 第 42 页。以下是一个概述:
键 | 类型 | 值 |
大小 | 整数 | 交叉引用表中的条目数 + 1。 |
前 | 整数 | 指向前一个xref 部分的偏移量(指示增量更新)。 |
根 | 字典 | (间接)指向目录的指针。参见前一节。 |
加密 | 字典 | 指向加密对象的指针(仅加密文件)。 |
信息 | 字典 | (间接)指向信息(元数据)的指针。 |
ID | 数组 | 由两个字节字符串组成的文件标识符。 |
XRefStm | int | 交叉引用流的偏移量。参见 Adobe PDF 参考手册 第 49 页。 |
通过 PyMuPDF 使用 Document.pdf_trailer()
或者等效地使用 Document.xref_object()
(使用 -1 而不是有效的 xref
编号)访问此信息。
>>> import pymupdf >>> doc=pymupdf.open("PyMuPDF.pdf") >>> print(doc.xref_object(-1)) # or: print(doc.pdf_trailer()) << /Type /XRef /Index [ 0 8263 ] /Size 8263 /W [ 1 3 1 ] /Root 8260 0 R /Info 8261 0 R /ID [ <4339B9CEE46C2CD28A79EBDDD67CC9B3> <4339B9CEE46C2CD28A79EBDDD67CC9B3> ] /Length 19883 /Filter /FlateDecode >> >>>
如何访问 XML 元数据
PDF 可以包含除标准元数据格式外的 XML 元数据。事实上,大多数 PDF 查看器或修改软件在保存 PDF 时添加此类信息(Adobe、Nitro PDF、PDF-XChange 等)。
PyMuPDF 无法直接解释或更改此信息,因为它不包含任何 XML 功能。但是,XML 元数据作为stream
对象存储,因此可以使用适当的软件进行读取、修改和重新写入。
>>> xmlmetadata = doc.get_xml_metadata() >>> print(xmlmetadata) <?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta x:xmptk="3.1-702"> <rdf:RDF > ... omitted data ... <?xpacket end="w"?>
使用某些 XML 软件包,可以解释和 / 或修改 XML 数据,然后存储回来。如果 PDF 之前没有 XML 元数据,以下内容也适用
>>> # write back modified XML metadata: >>> doc.set_xml_metadata(xmlmetadata) >>> >>> # XML metadata can be deleted like this: >>> doc.del_xml_metadata()
如何扩展 PDF 元数据
属性 Document.metadata
设计成对所有支持的文档类型都起作用:它是一个带有一组固定键值对的 Python 字典。相应地,Document.set_metadata()
仅接受标准键。
然而,PDF 可能包含无法像这样访问的项目。此外,可能存在存储额外信息的原因,例如版权信息。以下是使用 PyMuPDF 低级函数处理任意元数据项的方法。
例如,查看某些 PDF 的标准元数据输出:
# --------------------- # standard metadata # --------------------- pprint(doc.metadata) {'author': 'PRINCE', 'creationDate': "D:2010102417034406'-30'", 'creator': 'PrimoPDF http://www.primopdf.com/', 'encryption': None, 'format': 'PDF 1.4', 'keywords': '', 'modDate': "D:20200725062431-04'00'", 'producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' 'AppendMode 1.1', 'subject': '', 'title': 'Full page fax print', 'trapped': ''}
使用以下代码查看存储在元数据对象中的所有项:
# ---------------------------------- # metadata including private items # ---------------------------------- metadata = {} # make my own metadata dict what, value = doc.xref_get_key(-1, "Info") # /Info key in the trailer if what != "xref": pass # PDF has no metadata else: xref = int(value.replace("0 R", "")) # extract the metadata xref for key in doc.xref_get_keys(xref): metadata[key] = doc.xref_get_key(xref, key)[1] pprint(metadata) {'Author': 'PRINCE', 'CreationDate': "D:2010102417034406'-30'", 'Creator': 'PrimoPDF http://www.primopdf.com/', 'ModDate': "D:20200725062431-04'00'", 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb 9 ' "2015;12:00:06;D:20200725062431-04'00'", 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' 'AppendMode 1.1', 'Title': 'Full page fax print'} # --------------------------------------------------------------- # note the additional 'PXCViewerInfo' key - ignored in standard! # ---------------------------------------------------------------
反之亦然,您还可以在 PDF 中存储私有元数据项。您有责任确保这些项符合 PDF 规范 - 特别是它们必须是(unicode)字符串。有关详细信息和注意事项,请参阅 Adobe PDF 参考手册 第 14.3 节(第 548 页):
what, value = doc.xref_get_key(-1, "Info") # /Info key in the trailer if what != "xref": raise ValueError("PDF has no metadata") xref = int(value.replace("0 R", "")) # extract the metadata xref # add some private information doc.xref_set_key(xref, "mykey", pymupdf.get_pdf_str("北京 is Beijing")) # # after executing the previous code snippet, we will see this: pprint(metadata) {'Author': 'PRINCE', 'CreationDate': "D:2010102417034406'-30'", 'Creator': 'PrimoPDF http://www.primopdf.com/', 'ModDate': "D:20200725062431-04'00'", 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb 9 ' "2015;12:00:06;D:20200725062431-04'00'", 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' 'AppendMode 1.1', 'Title': 'Full page fax print', 'mykey': '北京 is Beijing'}
要删除选定的键,请使用 doc.xref_set_key(xref, "mykey", "null")
。如下一节所述,字符串“null”是 Python 的 None
的 PDF 等效项。具有该值的键将被视为未指定,并在垃圾回收中物理删除。
如何读取和更新 PDF 对象
还有精细、优雅的方式来访问和操作选定的 PDF dictionary
键。
Document.xref_get_keys()
返回对象在xref
处的 PDF 键:
In [1]: import pymupdf In [2]: doc = pymupdf.open("pymupdf.pdf") In [3]: page = doc[0] In [4]: from pprint import pprint In [5]: pprint(doc.xref_get_keys(page.xref)) ('Type', 'Contents', 'Resources', 'MediaBox', 'Parent')
- 与完整对象定义进行比较:
In [6]: print(doc.xref_object(page.xref)) << /Type /Page /Contents 1297 0 R /Resources 1296 0 R /MediaBox [ 0 0 612 792 ] /Parent 1301 0 R >>
- 单个键也可以直接通过
Document.xref_get_key()
访问。该值始终是一个字符串,包含有助于解释的类型信息:
In [7]: doc.xref_get_key(page.xref, "MediaBox") Out[7]: ('array', '[0 0 612 792]')
- 以下是上述页面键的完整列表:
In [9]: for key in doc.xref_get_keys(page.xref): ...: print("%s = %s" % (key, doc.xref_get_key(page.xref, key))) ...: Type = ('name', '/Page') Contents = ('xref', '1297 0 R') Resources = ('xref', '1296 0 R') MediaBox = ('array', '[0 0 612 792]') Parent = ('xref', '1301 0 R')
- 未定义的键查询返回
('null', 'null')
—— PDF 对象类型null
对应于 Python 中的None
。布尔值true
和false
也是类似的。 - 让我们向页面定义添加一个新的键,将其旋转至 90 度(您知道实际上存在
Page.set_rotation()
来完成此操作吗?):
In [11]: doc.xref_get_key(page.xref, "Rotate") # no rotation set: Out[11]: ('null', 'null') In [12]: doc.xref_set_key(page.xref, "Rotate", "90") # insert a new key In [13]: print(doc.xref_object(page.xref)) # confirm success << /Type /Page /Contents 1297 0 R /Resources 1296 0 R /MediaBox [ 0 0 612 792 ] /Parent 1301 0 R /Rotate 90 >>
- 通过将其值设置为
null
,此方法还可用于从xref
字典中删除一个键:以下示例将从页面中删除旋转规范:doc.xref_set_key(page.xref, "Rotate", "null")
。类似地,要从页面中删除所有链接、注释和字段,使用doc.xref_set_key(page.xref, "Annots", "null")
。因为Annots
按定义是一个数组,在这种情况下,通过语句doc.xref_set_key(page.xref, "Annots", "[]")
也会达到同样的效果。 - PDF 字典可以层次化嵌套。在以下页面对象定义中,字体和
XObject
都是Resources
的子字典:
In [15]: print(doc.xref_object(page.xref)) << /Type /Page /Contents 1297 0 R /Resources << /XObject << /Im1 1291 0 R >> /Font << /F39 1299 0 R /F40 1300 0 R >> >> /MediaBox [ 0 0 612 792 ] /Parent 1301 0 R /Rotate 90 >>
- 上述情况由方法
Document.xref_set_key()
和Document.xref_get_key()
支持:使用类似路径的表示法指向所需的键。例如,要检索上述键Im1
的值,请在键参数中指定完整的字典链“上方”:“Resources/XObject/Im1”。
In [16]: doc.xref_get_key(page.xref, "Resources/XObject/Im1") Out[16]: ('xref', '1291 0 R')
- 路径标记还可用于直接设置数值:使用以下方式让
Im1
指向不同的对象。
In [17]: doc.xref_set_key(page.xref, "Resources/XObject/Im1", "9999 0 R") In [18]: print(doc.xref_object(page.xref)) # confirm success: << /Type /Page /Contents 1297 0 R /Resources << /XObject << /Im1 9999 0 R >> /Font << /F39 1299 0 R /F40 1300 0 R >> >> /MediaBox [ 0 0 612 792 ] /Parent 1301 0 R /Rotate 90 >>
- 请注意,这里不会执行任何语义检查:如果 PDF 中没有 xref 9999,则此处不会检测到。
- 如果键不存在,将通过设置其值来创建它。此外,如果任何中间键也不存在,则会根据需要创建它们。以下示例在现有字典
A
的几个层次下创建了数组D
。中间字典B
和C
会自动创建:
In [5]: print(doc.xref_object(xref)) # some existing PDF object: << /A << >> >> In [6]: # the following will create 'B', 'C' and 'D' In [7]: doc.xref_set_key(xref, "A/B/C/D", "[1 2 3 4]") In [8]: print(doc.xref_object(xref)) # check out what happened: << /A << /B << /C << /D [ 1 2 3 4 ] >> >> >> >>
- 在设置键值时,MuPDF 将进行基本的PDF 语法检查。例如,新键只能在字典的下方创建。以下尝试在先前创建的数组
D
下创建一些新的字符串项E
:
In [9]: # 'D' is an array, no dictionary! In [10]: doc.xref_set_key(xref, "A/B/C/D/E", "(hello)") mupdf: not a dict (array) --- ... --- RuntimeError: not a dict (array)
- 也不可能在某些更高级别键是**“间接”**对象(即 xref)的情况下创建一个键。换句话说,xref 只能直接修改,而不能通过引用它们的其他对象隐式修改:
In [13]: # the following object points to an xref In [14]: print(doc.xref_object(4)) << /E 3 0 R >> In [15]: # 'E' is an indirect object and cannot be modified here! In [16]: doc.xref_set_key(4, "E/F", "90") mupdf: path to 'F' has indirects --- ... --- RuntimeError: path to 'F' has indirects
注意
这些是专家功能!不验证 PDF 对象、xrefs 等是否有效。与其他低级方法一样,有可能使 PDF 或其部分无法使用。
您对本页有任何反馈吗?
此软件按原样提供,不提供任何明示或暗示的担保。此软件在许可下分发,未经许可明确授权,不得复制、修改或分发。请参阅许可信息,访问artifex.com,或联系美国加利福尼亚州旧金山 94129 Mesa 街 39 号 108A 套房 Artifex Software Inc.获取更多信息。
此文档涵盖所有版本,直至 1.24.4。
常见问题及其解决方案
原文:
pymupdf.readthedocs.io/en/latest/recipes-common-issues-and-their-solutions.html
如何动态清理损坏的 PDF 文档
这展示了 PyMuPDF 与另一个 Python PDF 库的潜在用法(这里使用了优秀的纯 Python 包 pdfrw 作为示例)。
如果需要干净的、非损坏/解压缩的 PDF,可以动态调用 PyMuPDF 以解决许多问题,比如:
import sys from io import BytesIO from pdfrw import PdfReader import pymupdf #--------------------------------------- # 'Tolerant' PDF reader #--------------------------------------- def reader(fname, password = None): idata = open(fname, "rb").read() # read the PDF into memory and ibuffer = BytesIO(idata) # convert to stream if password is None: try: return PdfReader(ibuffer) # if this works: fine! except: pass # either we need a password or it is a problem-PDF # create a repaired / decompressed / decrypted version doc = pymupdf.open("pdf", ibuffer) if password is not None: # decrypt if password provided rc = doc.authenticate(password) if not rc > 0: raise ValueError("wrong password") c = doc.tobytes(garbage=3, deflate=True) del doc # close & delete doc return PdfReader(BytesIO(c)) # let pdfrw retry #--------------------------------------- # Main program #--------------------------------------- pdf = reader("pymupdf.pdf", password = None) # include a password if necessary print pdf.Info # do further processing
使用命令行实用程序 pdftk(仅适用 于 Windows,但据报道也可在 Wine 下运行),可以达到类似的结果,请参见 此处。但是,您必须通过 subprocess.Popen 作为独立进程调用它,使用 stdin 和 stdout 作为通信工具。
如何将任何文档转换为 PDF
下面是一个将任何 PyMuPDF 支持的文档 转换为 PDF 的脚本。这些包括 XPS、EPUB、FB2、CBZ 和包括多页 TIFF 图像在内的图像格式。
它支持维护源文档中包含的任何元数据、目录和链接:
""" Demo script: Convert input file to a PDF ----------------------------------------- Intended for multi-page input files like XPS, EPUB etc. Features: --------- Recovery of table of contents and links of input file. While this works well for bookmarks (outlines, table of contents), links will only work if they are not of type "LINK_NAMED". This link type is skipped by the script. For XPS and EPUB input, internal links however **are** of type "LINK_NAMED". Base library MuPDF does not resolve them to page numbers. So, for anyone expert enough to know the internal structure of these document types, can further interpret and resolve these link types. Dependencies -------------- PyMuPDF v1.14.0+ """ import sys import pymupdf if not (list(map(int, pymupdf.VersionBind.split("."))) >= [1,14,0]): raise SystemExit("need PyMuPDF v1.14.0+") fn = sys.argv[1] print("Converting '%s' to '%s.pdf'" % (fn, fn)) doc = pymupdf.open(fn) b = doc.convert_to_pdf() # convert to pdf pdf = pymupdf.open("pdf", b) # open as pdf toc= doc.get_toc() # table of contents of input pdf.set_toc(toc) # simply set it for output meta = doc.metadata # read and set metadata if not meta["producer"]: meta["producer"] = "PyMuPDF v" + pymupdf.VersionBind if not meta["creator"]: meta["creator"] = "PyMuPDF PDF converter" meta["modDate"] = pymupdf.get_pdf_now() meta["creationDate"] = meta["modDate"] pdf.set_metadata(meta) # now process the links link_cnti = 0 link_skip = 0 for pinput in doc: # iterate through input pages links = pinput.get_links() # get list of links link_cnti += len(links) # count how many pout = pdf[pinput.number] # read corresp. output page for l in links: # iterate though the links if l["kind"] == pymupdf.LINK_NAMED: # we do not handle named links print("named link page", pinput.number, l) link_skip += 1 # count them continue pout.insert_link(l) # simply output the others # save the conversion result pdf.save(fn + ".pdf", garbage=4, deflate=True) # say how many named links we skipped if link_cnti > 0: print("Skipped %i named links of a total of %i in input." % (link_skip, link_cnti))
如何处理 MuPDF 发出的消息
自 PyMuPDF v1.16.0 起,由底层 MuPDF 库发出的 错误消息 被重定向到 Python 标准设备 sys.stderr。因此,您可以像处理任何其他输出一样处理它们。
此外,这些消息与任何 MuPDF 警告一起进入内部缓冲区,详情见下文。
我们始终使用识别字符串 “mupdf:” 作为这些消息的前缀。如果您不想看到可恢复的 MuPDF 错误消息,可以执行命令 pymupdf.TOOLS.mupdf_display_errors(False)
。
MuPDF 警告继续存储在内部缓冲区中,并可以使用 Tools.mupdf_warnings()
查看。
请注意,MuPDF 错误可能会导致或不会导致 Python 异常。换句话说,您可能会看到 MuPDF 可以恢复并继续处理的错误消息。
可恢复错误的示例输出。我们打开了一个损坏的 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 >>>
PyMuPDF 1.24.4 中文文档(五)(5)https://developer.aliyun.com/article/1559440