PyMuPDF-1-24-4-中文文档-五-
PyMuPDF 1.24.4 中文文档(五)
记录
原文:
pymupdf.readthedocs.io/en/latest/recipes-journalling.html
从版本 1.19.0 开始,可以在更新 PDF 文档时记录日志。
记录是一种允许在 PDF 中回滚或重新应用更改的日志记录机制。类似于现代数据库系统中的 LUW“逻辑工作单元”,可以将一组更新分组成一个“操作”。在 MuPDF 记录中,操作扮演着 LUW 的角色。
注意
与数据库系统中找到的 LUW 实现相比,MuPDF 记录发生在每个文档级别上。不支持跨多个 PDF 同时更新:必须在此处建立自己的逻辑。
-
必须通过文档方法启用记录。可以为现有或新文档记录。只能通过关闭文件来禁用记录。
-
一旦启用,每个更改必须发生在一个操作内 —— 否则会引发异常。通过文档方法启动和停止操作。在这两次调用之间发生的更新形成一个 LUW,因此可以集体回滚或重新应用,或者用 MuPDF 术语来说是“撤销”或“重做”。
-
在任何时候,都可以查询记录状态:记录是否活动,已记录多少操作,是否可能“撤消”或“重做”,日志中的当前位置等。
-
日志可以保存到或从文件加载。这些是文档方法。
-
加载日志文件时,会检查与文档的兼容性,并在成功时自动启用日志记录。
-
对于正在记录日志的现有 PDF,提供了一种特殊的新保存方法:
Document.save_snapshot()
。这执行特殊的增量保存,包括到目前为止的所有记录更新。如果同时保存其日志(在文档快照之后立即进行),则文档和日志将同步,以便随后可以一起用于撤消或重做操作或继续记录的更新 —— 就像没有中断一样。 -
快照 PDF 在各方面都是有效的 PDF,完全可用。但是,如果在没有使用其日志文件的情况下以任何方式更改文档,则会发生失同步,日志将变得无法使用。
-
快照文件的结构类似增量更新。然而,内部记录日志的逻辑要求,保存必须发生到一个新文件中。因此,用户应该开发文件命名约定,以支持原始 PDF(如
original.pdf
)与其快照集之间的可识别关系,如original-snap1.pdf
/original-snap1.log
、original-snap2.pdf
/original-snap2.log
等。
会话示例 1
描述:
-
制作一个新的 PDF 并启用记录。然后添加一个页面和一些文本行 —— 每个作为单独的操作。
-
在日志中导航,撤消和重做这些更新,并显示状态和文件结果:
>>> import pymupdf >>> doc=pymupdf.open() >>> doc.journal_enable() >>> # try update without an operation: >>> page = doc.new_page() mupdf: No journalling operation started ... omitted lines RuntimeError: No journalling operation started >>> doc.journal_start_op("op1") >>> page = doc.new_page() >>> doc.journal_stop_op() >>> doc.journal_start_op("op2") >>> page.insert_text((100,100), "Line 1") >>> doc.journal_stop_op() >>> doc.journal_start_op("op3") >>> page.insert_text((100,120), "Line 2") >>> doc.journal_stop_op() >>> doc.journal_start_op("op4") >>> page.insert_text((100,140), "Line 3") >>> doc.journal_stop_op() >>> # show position in journal >>> doc.journal_position() (4, 4) >>> # 4 operations recorded - positioned at bottom >>> # what can we do? >>> doc.journal_can_do() {'undo': True, 'redo': False} >>> # currently only undos are possible. Print page content: >>> print(page.get_text()) Line 1 Line 2 Line 3 >>> # undo last insert: >>> doc.journal_undo() >>> # show combined status again: >>> doc.journal_position();doc.journal_can_do() (3, 4) {'undo': True, 'redo': True} >>> print(page.get_text()) Line 1 Line 2 >>> # our position is now second to last >>> # last text insertion was reverted >>> # but we can redo / move forward as well: >>> doc.journal_redo() >>> # our combined status: >>> doc.journal_position();doc.journal_can_do() (4, 4) {'undo': True, 'redo': False} >>> print(page.get_text()) Line 1 Line 2 Line 3 >>> # line 3 has appeared again!
示例会话 2
描述:
-
类似于之前的情况,但在撤消某些操作后,我们现在添加了不同的更新。这将导致:
- 永久删除未完成的日志条目
- 新的更新操作将成为新的最后条目。
>>> doc=pymupdf.open() >>> doc.journal_enable() >>> doc.journal_start_op("Page insert") >>> page=doc.new_page() >>> doc.journal_stop_op() >>> for i in range(5): doc.journal_start_op("insert-%i" % i) page.insert_text((100, 100 + 20*i), "text line %i" %i) doc.journal_stop_op()
>>> # combined status info: >>> doc.journal_position();doc.journal_can_do() (6, 6) {'undo': True, 'redo': False}
>>> for i in range(3): # revert last three operations doc.journal_undo() >>> doc.journal_position();doc.journal_can_do() (3, 6) {'undo': True, 'redo': True}
>>> # now do a different update: >>> doc.journal_start_op("Draw some line") >>> page.draw_line((100,150), (300,150)) Point(300.0, 150.0) >>> doc.journal_stop_op() >>> doc.journal_position();doc.journal_can_do() (4, 4) {'undo': True, 'redo': False}
>>> # this has changed the journal: >>> # previous last 3 text line operations were removed, and >>> # we have only 4 operations: drawing the line is the new last one
对本页面有任何反馈吗?
此软件按原样提供,不提供任何明示或暗示的保证。此软件按许可分发,并且未经许可明确授权的情况下,不得复制、修改或分发该软件。有关许可信息,请参阅 artifex.com 或联系 Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, 美国以获取更多信息。
此文档覆盖所有版本,直至 1.24.4。
示例会话 1
描述:
-
创建一个新的 PDF 并启用日志记录。然后添加一个页面和一些文本行 - 每个都作为单独的操作。
-
在日志中导航,撤消和重做这些更新,并显示状态和文件结果:
>>> import pymupdf >>> doc=pymupdf.open() >>> doc.journal_enable() >>> # try update without an operation: >>> page = doc.new_page() mupdf: No journalling operation started ... omitted lines RuntimeError: No journalling operation started >>> doc.journal_start_op("op1") >>> page = doc.new_page() >>> doc.journal_stop_op() >>> doc.journal_start_op("op2") >>> page.insert_text((100,100), "Line 1") >>> doc.journal_stop_op() >>> doc.journal_start_op("op3") >>> page.insert_text((100,120), "Line 2") >>> doc.journal_stop_op() >>> doc.journal_start_op("op4") >>> page.insert_text((100,140), "Line 3") >>> doc.journal_stop_op() >>> # show position in journal >>> doc.journal_position() (4, 4) >>> # 4 operations recorded - positioned at bottom >>> # what can we do? >>> doc.journal_can_do() {'undo': True, 'redo': False} >>> # currently only undos are possible. Print page content: >>> print(page.get_text()) Line 1 Line 2 Line 3 >>> # undo last insert: >>> doc.journal_undo() >>> # show combined status again: >>> doc.journal_position();doc.journal_can_do() (3, 4) {'undo': True, 'redo': True} >>> print(page.get_text()) Line 1 Line 2 >>> # our position is now second to last >>> # last text insertion was reverted >>> # but we can redo / move forward as well: >>> doc.journal_redo() >>> # our combined status: >>> doc.journal_position();doc.journal_can_do() (4, 4) {'undo': True, 'redo': False} >>> print(page.get_text()) Line 1 Line 2 Line 3 >>> # line 3 has appeared again!
示例会话 2
描述:
-
类似于之前的情况,但在撤消某些操作后,我们现在添加了不同的更新。这将导致:
- 永久删除未完成的日志条目
- 新的更新操作将成为新的最后条目。
>>> doc=pymupdf.open() >>> doc.journal_enable() >>> doc.journal_start_op("Page insert") >>> page=doc.new_page() >>> doc.journal_stop_op() >>> for i in range(5): doc.journal_start_op("insert-%i" % i) page.insert_text((100, 100 + 20*i), "text line %i" %i) doc.journal_stop_op()
>>> # combined status info: >>> doc.journal_position();doc.journal_can_do() (6, 6) {'undo': True, 'redo': False}
>>> for i in range(3): # revert last three operations doc.journal_undo() >>> doc.journal_position();doc.journal_can_do() (3, 6) {'undo': True, 'redo': True}
>>> # now do a different update: >>> doc.journal_start_op("Draw some line") >>> page.draw_line((100,150), (300,150)) Point(300.0, 150.0) >>> doc.journal_stop_op() >>> doc.journal_position();doc.journal_can_do() (4, 4) {'undo': True, 'redo': False}
>>> # this has changed the journal: >>> # previous last 3 text line operations were removed, and >>> # we have only 4 operations: drawing the line is the new last one
对本页面有任何反馈吗?
此软件按原样提供,不提供任何明示或暗示的保证。此软件按许可分发,并且未经许可明确授权的情况下,不得复制、修改或分发该软件。有关许可信息,请参阅 artifex.com 或联系 Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, 美国以获取更多信息。
此文档覆盖所有版本,直至 1.24.4。
多进程
原文:
pymupdf.readthedocs.io/en/latest/recipes-multiprocessing.html
MuPDF 没有集成支持线程 - 自称“线程不可知”。虽然 MuPDF 还存在一些复杂的可能性可以使用线程,但对于 PyMuPDF 的基本结果是:
不支持 Python 线程。
在 Python 线程环境中使用 PyMuPDF 会导致主线程阻塞。
但是,您可以选择使用 Python 的 multiprocessing 模块以各种方式。
如果您希望加快大型文档的页面处理速度,请将此脚本用作起点。它的速度至少应该比对应的顺序处理快两倍。
查看代码
"""
Demonstrate the use of multiprocessing with PyMuPDF.
Depending on the number of CPUs, the document is divided in page ranges.
Each range is then worked on by one process.
The type of work would typically be text extraction or page rendering. Each
process must know where to put its results, because this processing pattern
does not include inter-process communication or data sharing.
Compared to sequential processing, speed improvements in range of 100% (ie.
twice as fast) or better can be expected.
"""
from __future__ import print_function, division
import sys
import os
import time
from multiprocessing import Pool, cpu_count
import pymupdf
# choose a version specific timer function (bytes == str in Python 2)
mytime = time.clock if str is bytes else time.perf_counter
def render_page(vector):
"""Render a page range of a document.
Notes:
The PyMuPDF document cannot be part of the argument, because that
cannot be pickled. So we are being passed in just its filename.
This is no performance issue, because we are a separate process and
need to open the document anyway.
Any page-specific function can be processed here - rendering is just
an example - text extraction might be another.
The work must however be self-contained: no inter-process communication
or synchronization is possible with this design.
Care must also be taken with which parameters are contained in the
argument, because it will be passed in via pickling by the Pool class.
So any large objects will increase the overall duration.
Args:
vector: a list containing required parameters.
"""
# recreate the arguments
idx = vector[0] # this is the segment number we have to process
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
mat = vector[3] # the matrix for rendering
doc = pymupdf.open(filename) # open the document
num_pages = doc.page_count # get number of pages
# pages per segment: make sure that cpu * seg_size >= num_pages!
seg_size = int(num_pages / cpu + 1)
seg_from = idx * seg_size # our first page number
seg_to = min(seg_from + seg_size, num_pages) # last page number
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
# page.get_text("rawdict") # use any page-related type of work here, eg
pix = page.get_pixmap(alpha=False, matrix=mat)
# store away the result somewhere ...
# pix.save("p-%i.png" % i)
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
if __name__ == "__main__":
t0 = mytime() # start a timer
filename = sys.argv[1]
mat = pymupdf.Matrix(0.2, 0.2) # the rendering matrix: scale down to 20%
cpu = cpu_count()
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, mat) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
pool = Pool() # make pool of 'cpu_count()' processes
pool.map(render_page, vectors, 1) # start processes passing each a vector
t1 = mytime() # stop the timer
print("Total time %g seconds" % round(t1 - t0, 2))
这是一个更复杂的示例,涉及主进程(显示 GUI)和子进程(执行 PyMuPDF 访问文档)之间的进程间通信。
查看代码
"""
Created on 2019-05-01
@author: yinkaisheng@live.com
@copyright: 2019 yinkaisheng@live.com
@license: GNU AFFERO GPL 3.0
Demonstrate the use of multiprocessing with PyMuPDF
-----------------------------------------------------
This example shows some more advanced use of multiprocessing.
The main process show a Qt GUI and establishes a 2-way communication with
another process, which accesses a supported document.
"""
import os
import sys
import time
import multiprocessing as mp
import queue
import pymupdf
''' PyQt and PySide namespace unifier shim
https://www.pythonguis.com/faq/pyqt6-vs-pyside6/
simple "if 'PyQt6' in sys.modules:" test fails for me, so the more complex pkgutil use
overkill for most people who might have one or the other, why both?
'''
from pkgutil import iter_modules
def module_exists(module_name):
return module_name in (name for loader, name, ispkg in iter_modules())
if module_exists("PyQt6"):
# PyQt6
from PyQt6 import QtGui, QtWidgets, QtCore
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
wrapper = "PyQt6"
elif module_exists("PySide6"):
# PySide6
from PySide6 import QtGui, QtWidgets, QtCore
from PySide6.QtCore import Signal, Slot
wrapper = "PySide6"
my_timer = time.clock if str is bytes else time.perf_counter
class DocForm(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.process = None
self.queNum = mp.Queue()
self.queDoc = mp.Queue()
self.page_count = 0
self.curPageNum = 0
self.lastDir = ""
self.timerSend = QtCore.QTimer(self)
self.timerSend.timeout.connect(self.onTimerSendPageNum)
self.timerGet = QtCore.QTimer(self)
self.timerGet.timeout.connect(self.onTimerGetPage)
self.timerWaiting = QtCore.QTimer(self)
self.timerWaiting.timeout.connect(self.onTimerWaiting)
self.initUI()
def initUI(self):
vbox = QtWidgets.QVBoxLayout()
self.setLayout(vbox)
hbox = QtWidgets.QHBoxLayout()
self.btnOpen = QtWidgets.QPushButton("OpenDocument", self)
self.btnOpen.clicked.connect(self.openDoc)
hbox.addWidget(self.btnOpen)
self.btnPlay = QtWidgets.QPushButton("PlayDocument", self)
self.btnPlay.clicked.connect(self.playDoc)
hbox.addWidget(self.btnPlay)
self.btnStop = QtWidgets.QPushButton("Stop", self)
self.btnStop.clicked.connect(self.stopPlay)
hbox.addWidget(self.btnStop)
self.label = QtWidgets.QLabel("0/0", self)
self.label.setFont(QtGui.QFont("Verdana", 20))
hbox.addWidget(self.label)
vbox.addLayout(hbox)
self.labelImg = QtWidgets.QLabel("Document", self)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding
)
self.labelImg.setSizePolicy(sizePolicy)
vbox.addWidget(self.labelImg)
self.setGeometry(100, 100, 400, 600)
self.setWindowTitle("PyMuPDF Document Player")
self.show()
def openDoc(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"Open Document",
self.lastDir,
"All Supported Files (*.pdf;*.epub;*.xps;*.oxps;*.cbz;*.fb2);;PDF Files (*.pdf);;EPUB Files (*.epub);;XPS Files (*.xps);;OpenXPS Files (*.oxps);;CBZ Files (*.cbz);;FB2 Files (*.fb2)",
#options=QtWidgets.QFileDialog.Options(),
)
if path:
self.lastDir, self.file = os.path.split(path)
if self.process:
self.queNum.put(-1) # use -1 to notify the process to exit
self.timerSend.stop()
self.curPageNum = 0
self.page_count = 0
self.process = mp.Process(
target=openDocInProcess, args=(path, self.queNum, self.queDoc)
)
self.process.start()
self.timerGet.start(40)
self.label.setText("0/0")
self.queNum.put(0)
self.startTime = time.perf_counter()
self.timerWaiting.start(40)
def playDoc(self):
self.timerSend.start(500)
def stopPlay(self):
self.timerSend.stop()
def onTimerSendPageNum(self):
if self.curPageNum < self.page_count - 1:
self.queNum.put(self.curPageNum + 1)
else:
self.timerSend.stop()
def onTimerGetPage(self):
try:
ret = self.queDoc.get(False)
if isinstance(ret, int):
self.timerWaiting.stop()
self.page_count = ret
self.label.setText("{}/{}".format(self.curPageNum + 1, self.page_count))
else: # tuple, pixmap info
num, samples, width, height, stride, alpha = ret
self.curPageNum = num
self.label.setText("{}/{}".format(self.curPageNum + 1, self.page_count))
fmt = (
QtGui.QImage.Format.Format_RGBA8888
if alpha
else QtGui.QImage.Format.Format_RGB888
)
qimg = QtGui.QImage(samples, width, height, stride, fmt)
self.labelImg.setPixmap(QtGui.QPixmap.fromImage(qimg))
except queue.Empty as ex:
pass
def onTimerWaiting(self):
self.labelImg.setText(
'Loading "{}", {:.2f}s'.format(
self.file, time.perf_counter() - self.startTime
)
)
def closeEvent(self, event):
self.queNum.put(-1)
event.accept()
def openDocInProcess(path, queNum, quePageInfo):
start = my_timer()
doc = pymupdf.open(path)
end = my_timer()
quePageInfo.put(doc.page_count)
while True:
num = queNum.get()
if num < 0:
break
page = doc.load_page(num)
pix = page.get_pixmap()
quePageInfo.put(
(num, pix.samples, pix.width, pix.height, pix.stride, pix.alpha)
)
doc.close()
print("process exit")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
form = DocForm()
sys.exit(app.exec())
你对这个页面有任何反馈吗?
本软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的。本软件受许可分发,未经授权不得复制、修改或分发。请参阅许可信息 artifex.com 或联系位于美国加利福尼亚州圣弗朗西斯科市 Mesa Street 39 号 108A 套房的 Artifex Software Inc. 获取更多信息。
本文档覆盖了所有 1.24.4 版本及更早版本。
OCR - 光学字符识别
PyMuPDF 已经集成了 OCR(光学字符识别)支持。可以使用 OCR 来处理图像(通过 Pixmap 类)和文档页面。
此功能目前基于必须作为单独应用程序安装的 Tesseract-OCR - 请参阅安装章节。
如何 OCR 一张图片
必须先将支持的图像转换为 Pixmap。然后,可以将该像素图保存为 1 页 PDF。此页面将与原始图像具有相同的宽度和高度。它将包含由 Tesseract 识别的文本层。
可以通过Pixmap.pdfocr_save()
或Pixmap.pdfocr_tobytes()
方法之一生成 PDF,保存为磁盘上的文件或内存中的 PDF。
文本可以使用常规文本提取和搜索方法(Page.get_text()
,Page.search_for()
等)提取和搜索。还请注意以下重要事实和先决条件:
-
当将图像转换为像素图时,请确认颜色空间为 RGB,alpha 为
False
(无透明度)。如有必要,转换原始像素图。 -
所有文本都以 Tesseract 的自有
GlyphLessFont
书写,这是一种与 Courier 类似的等宽字体。 -
所有文本都具有常规和黑色的属性(即不加粗,不斜体,没有关于原始字体的信息)。
-
Tesseract 不识别矢量图形(即没有绘图/线条艺术)。
这种方法也建议用于 OCR 完整的扫描 PDF:
-
将每一页渲染为具有所需分辨率的 Pixmap
-
将生成的 1 页 PDF 追加到输出 PDF 中
如何 OCR 文档页面
任何支持的文档页面都可以进行 OCR 处理 - 无论是完整页面还是仅其中的图像区域。
由于光学字符识别的速度约为标准文本提取的一千倍,我们确保每页仅进行一次 OCR,并将结果存储在 TextPage 中。使用此 TextPage 进行所有后续的提取和文本搜索将使用 PyMuPDF 的常规顶级速度进行。
要 OCR 文档页面,请按照以下步骤操作:
- 确定是否需要/有利于使用 OCR。可以使用多个标准来做出此决定,例如:
- 页面完全被图像覆盖
- 页面上不存在文本
- 数千个小型矢量图形(表示模拟文本)
-
OCR 该页面并使用类似
tp = page.get_textpage_ocr(...)
的指令将结果存储在 TextPage 对象中。 -
在所有后续文本提取和搜索中引用生成的 TextPage,使用
textpage=tp
参数。
您对本页有任何反馈吗?
本软件按“原样”提供,不附带任何明示或暗示的保证。本软件根据许可协议分发,未经授权不得复制、修改或分发。请参阅 artifex.com 的许可信息,或联系 Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, United States 获取更多信息。
本文档覆盖至 1.24.4 版本。
如何对图像进行 OCR
支持的图像必须首先转换为 Pixmap。然后可以将该 Pixmap 保存为 1 页 PDF。此页面将与原始图像具有相同的宽度和高度,并包含由 Tesseract 识别的文本层。
可以通过其中一种方法生成 PDF 文件:Pixmap.pdfocr_save()
或 Pixmap.pdfocr_tobytes()
,可以生成磁盘上的文件或内存中的 PDF。
可以使用通常的文本提取和搜索方法(Page.get_text()
、Page.search_for()
等)提取和搜索文本。还请注意以下重要事实和前提条件:
-
在将图像转换为 Pixmap 时,请确认颜色空间为 RGB,透明度为
False
(无透明度)。必要时转换原始 Pixmap。 -
所有文本都使用 Tesseract 自有的
GlyphLessFont
写成,这是一种具有与 Courier 类似度量的等宽字体。 -
所有文本均具有常规和黑色属性(即没有粗体,没有斜体,也没有原始字体信息)。
-
Tesseract 无法识别矢量图形(即没有绘图/线条艺术)。
对于 OCR 完整的扫描 PDF,也建议采用这种方法:
-
将每页渲染为所需分辨率的 Pixmap
-
将生成的 1 页 PDF 追加到输出 PDF 中。
如何对文档页面进行 OCR
任何支持的文档页面都可以进行 OCR 处理 —— 可以是整个页面,也可以只是其中的图像区域。
由于光学字符识别比标准文本提取慢大约一千倍,我们确保每页仅进行一次 OCR,并将结果存储在 TextPage 中。使用此 TextPage 进行所有后续的提取和文本搜索将以 PyMuPDF 的常规最高速度进行。
要对文档页面进行 OCR,请按以下步骤操作:
- 确定是否需要或者说 OCR 是否有益。可以根据一些标准做出决定,例如:
- 页面完全被图像覆盖。
- 页面上不存在文本
- 数千个小型矢量图形(表示模拟文本)
-
OCR 页面并使用类似
tp = page.get_textpage_ocr(...)
的指令将结果存储在 TextPage 对象中。 -
在所有后续的文本提取和搜索中,通过
textpage=tp
参数引用生成的 TextPage。
对于这个页面,你有任何反馈吗?
本软件按原样提供,不附带任何明示或暗示的保证。本软件在许可下分发,除非在该许可条款明确授权下,否则不得复制、修改或分发。请参阅artifex.com获取许可信息或联系美国加利福尼亚州旧金山 94129 Mesa 街 39 号 108A 套房的 Artifex Software Inc.获取更多信息。
本文档涵盖了所有版本直至 1.24.4。
可选内容支持
原文:
pymupdf.readthedocs.io/en/latest/recipes-optional-content.html
本文解释了 PyMuPDF 对 PDF 概念“可选内容”的支持。
介绍:可选内容概念
在 PDF 中的可选内容是根据某些条件显示或隐藏文档部分的一种方式:当使用支持的 PDF 消费者(查看器)或通过编程设置参数为 ON 或 OFF 时。
这种能力在诸如 CAD 图纸、分层艺术品、地图和多语言文档等项目中非常有用。典型用途包括显示或隐藏复杂矢量图形的细节,例如地理地图、技术设备、建筑设计等,包括在屏幕显示文档与打印文档时自动切换不同的缩放级别。其他用例可能包括在屏幕上显示文档时自动显示不同的详细级别,而不是打印时。
特殊的 PDF 对象,称为可选内容组(OCG),用于定义这些不同层的内容。
将 OCG 分配给“普通”PDF 对象(如文本或图像)将导致该对象根据分配的 OCG 的当前状态而显示或隐藏。
为了简化 PDF 可选内容的总体配置定义,OCG 可以组织成更高级别的分组,称为OC 配置。每个配置都是 OCG 的集合,以及每个 OCG 所需的初始可见性状态。选择其中一个配置(通过 PDF 查看器或通过编程方式)将导致整个文档中所有受影响的 PDF 对象的相应可见性变化。
除了默认的 OC 配置外,OC 配置都是可选的。
对于更多解释和额外背景,请参考 PDF 规范手册。
PyMuPDF 对 PDF 可选内容的支持
PyMuPDF 完全支持查看、定义、更改和删除选项内容组、配置,并维护 OCG 分配给 PDF 对象以及通过编程方式在 OC 配置和每个单独 OCG 的可见性状态之间切换。
如何添加可选内容
这与向 PDF 添加一个可选内容组(OCG)一样简单:Document.add_ocg()
。
如果之前的 PDF 完全没有 OC 支持,那么所需的设置(如定义默认的 OC 配置)将在此自动完成。
该方法返回创建的 OCG 的xref
。使用此 xref 将任何想要依赖于此 OCG 状态的 PDF 对象关联(标记)。例如,您可以在页面上插入一幅图像,并像这样引用 xref:
img_xref = page.insert_image(rect, filename="image.file", oc=xref)
如果您想要将一个现有图像置于 OCG 的控制之下,必须首先找出图像的 xref 编号(这里称为 img_xref
),然后执行 doc.set_oc(img_xref, xref)
。这之后,只要 OCG 的状态为“ON”,图像就会在整个文档中可见或不可见。您还可以使用此方法分配不同的 OCG。
要从图像中移除一个 OCG,请执行 doc.set_oc(img_xref, 0)
。
可以将一个单独的 OCG 分配给多个 PDF 对象以控制它们的可见性。
如何定义复杂的可选内容条件
可以建立复杂的逻辑条件来满足复杂的可见性需求。
例如,您可能希望创建一个多语言文档,以便用户根据需要切换语言。
请查看这个 Jupyter Notebook,根据需要执行它。
当然,您的需求可能更复杂,并涉及多个通过某种逻辑关系连接的 ON/OFF 状态的 OCGs,但这应该能让您对可能性有所了解,并且帮助您计划下一步操作。
您对本页面有何反馈?
本软件按原样提供,不附带任何明示或暗示的担保。本软件在许可证下分发,并且除非在该许可证的条款明确授权下,否则不得复制、修改或分发。请参阅许可信息,网址为artifex.com,或联系美国加利福尼亚州旧金山市 Mesa 街 39 号 108A 室的 Artifex Software Inc. 获取更多信息。
本文档涵盖了所有版本直至 1.24.4。
引言:可选内容概念
PDF 中的可选内容是根据特定条件显示或隐藏文档部分的一种方法:当使用支持的 PDF 消费者(查看器)或通过编程设置参数为 ON 或 OFF 时。
这种功能在诸如 CAD 图纸、分层艺术品、地图和多语言文档等项目中非常有用。典型的用途包括显示或隐藏复杂矢量图形的详细信息,例如地理地图、技术设备、建筑设计等,还包括在屏幕上显示文档与打印文档时自动切换不同缩放级别。其他用例可能是在显示文档时自动显示不同的详细级别,而不是打印它。
特殊的 PDF 对象,称为可选内容组(OCG),用于定义这些不同的内容层。
将 OCG 分配给“普通”PDF 对象(如文本或图像)会导致该对象根据所分配的 OCG 的当前状态可见或隐藏。
为了简化 PDF 可选内容的总体配置定义,OCGs 可以组织在称为OC 配置的更高级别分组中。每个配置都是 OCG 的集合,以及每个 OCG 的期望初始可见性状态。通过 PDF 查看器或以编程方式选择其中一个配置,会导致整个文档中所有受影响的 PDF 对象的相应可见性变化。
除了默认配置外,OC 配置是可选的。
若要获得更多解释和额外背景,请参考 PDF 规范手册。
PyMuPDF 支持 PDF 可选内容
PyMuPDF 提供完整支持,可用于查看、定义、更改和删除可选内容组、配置,以及维护 OCG 与 PDF 对象的分配,并以编程方式在 OC 配置和每个单独 OCG 的可见性状态之间进行切换。
如何添加可选内容
这就像将一个 Optional Content Group(OCG)添加到 PDF 文档中一样简单:Document.add_ocg()
。
如果之前 PDF 完全不支持 OC,则在此时会自动完成所需的设置(如定义默认的 OC 配置)。
该方法返回一个xref
,表示创建的 OCG 的引用。使用此 xref 来将任何 PDF 对象与之关联(标记),你希望使其依赖于此 OCG 的状态。例如,你可以在页面上插入一张图像,并像这样引用 xref:
img_xref = page.insert_image(rect, filename="image.file", oc=xref)
如果要将现有图像放在 OCG 的控制下,必须首先找出图像的 xref 号(这里称为img_xref
),然后执行doc.set_oc(img_xref, xref)
。之后,如果 OCG 的状态为“ON”或“OFF”,则该图像将在整个文档中可见或不可见。您还可以使用此方法分配不同的 OCG。
要从图像中移除一个 OCG,执行doc.set_oc(img_xref, 0)
。
可以将一个单独的 OCG 分配给多个 PDF 对象以控制它们的可见性。
如何定义复杂的可选内容条件
复杂的逻辑条件可以建立来解决复杂的可见性需求。
例如,您可能希望创建一个多语言文档,以便用户可以根据需要切换语言。
请查看此 Jupyter Notebook,并按需执行。
当然,您的需求可能更复杂,涉及多个 OCG 及其通过某种逻辑关系连接的 ON/OFF 状态,但这应该让您了解到可能性及如何规划下一步操作。
对于此页面有任何反馈吗?
该软件按原样提供,不提供任何形式的明示或暗示保证。该软件根据许可证分发,未经明确授权,不得复制、修改或分发。请参阅 artifex.com 上的许可信息,或联系美国加利福尼亚州旧金山市 Mesa Street 39 号 108A 室的 Artifex Software Inc. 了解更多信息。
这份文档涵盖了截止到 1.24.4 版本的所有内容。
低级接口
原文:
pymupdf.readthedocs.io/en/latest/recipes-low-level-interfaces.html
有许多方法可以在相对低级别上访问和操作 PDF 文件。诚然,“低级别”和“常规”功能之间的明确区分并非总是可能的,也是主观品味的问题。
也可能发生之前被认为是低级的功能后来被评估为是正常接口的一部分。在 v1.14.0 版本中,Tools 类已经发生了这种情况 —— 现在它作为“类”章节中的一个条目找到。
只需查找文档的哪个章节,就能找到所需内容,这仅仅是文档问题。所有内容都可以通过同一界面随时获取。
如何迭代通过xref
表
PDF 的xref
表是文件中定义的所有对象的列表。该表可能包含许多条目 — 例如,Adobe PDF 参考手册中有 127,000 个对象。表条目“0”被保留,不得触及。以下脚本循环遍历xref
表,并打印每个对象的定义:
>>> xreflen = doc.xref_length() # length of objects table
>>> for xref in range(1, xreflen): # skip item 0!
print("")
print("object %i (stream: %s)" % (xref, doc.xref_is_stream(xref)))
print(doc.xref_object(xref, compressed=False))
以下内容将被生成:
object 1 (stream: False)
<<
/ModDate (D:20170314122233-04'00')
/PXCViewerInfo (PDF-XChange Viewer;2.5.312.1;Feb 9 2015;12:00:06;D:20170314122233-04'00')
>>
object 2 (stream: False)
<<
/Type /Catalog
/Pages 3 0 R
>>
object 3 (stream: False)
<<
/Kids [ 4 0 R 5 0 R ]
/Type /Pages
/Count 2
>>
object 4 (stream: False)
<<
/Type /Page
/Annots [ 6 0 R ]
/Parent 3 0 R
/Contents 7 0 R
/MediaBox [ 0 0 595 842 ]
/Resources 8 0 R
>>
...
object 7 (stream: True)
<<
/Length 494
/Filter /FlateDecode
>>
...
PDF 对象定义是一个普通的 ASCII 字符串。
如何处理对象流
除对象定义外,某些对象类型还包含额外数据。例如,图片、字体、嵌入文件或描述页面外观的命令。
这些类型的对象称为“流对象”。PyMuPDF 允许通过方法Document.xref_stream()
读取对象的流,参数为对象的xref
。还可以使用Document.update_stream()
写回流的修改版本。
假设以下代码片段想要读取 PDF 的所有流,无论出于何种原因:
>>> xreflen = doc.xref_length() # number of objects in file
>>> for xref in range(1, xreflen): # skip item 0!
if stream := doc.xref_stream(xref):
# do something with it (it is a bytes object or None)
# e.g. just write it back:
doc.update_stream(xref, stream)
Document.xref_stream()
自动以字节对象形式返回解压的流 — 而Document.update_stream()
如果有益,也会自动压缩它。
如何处理页面内容
PDF 页面可以有零个或多个contents
对象。这些是描述页面上出现的内容及其方式(如文本和图像)的流对象。它们以特殊的迷你语言编写,如 Adobe PDF 参考手册中第 643 页的附录 A“操作符概要”中所述。
每个 PDF 阅读应用程序必须能够解释内容语法,以重现页面的预期外观。
如果提供了多个contents
对象,则必须按照指定的顺序解释它们,就像它们是按顺序串联起来的一样。
对于拥有多个contents
对象存在一些技术上的优点:
-
仅仅添加新的
contents
对象要比维护单个大对象更加简单和快速(后者涉及每次更改都需要读取、解压缩、修改、重新压缩和重写)。 -
当使用增量更新时,修改过的大型
contents
对象会增加更新的增量,因此很容易抵消增量保存的效率。
例如,PyMuPDF 在Page.insert_image()
、Page.show_pdf_page()
方法以及 Shape 方法中添加新的小型contents
对象。
然而,也有一些情况下单个contents
对象更有利:它比多个较小的对象更容易解释和更易于压缩。
这里有两种合并页面多个内容的方法:
>>> # method 1: use the MuPDF clean function
>>> page.clean_contents() # cleans and combines multiple Contents
>>> xref = page.get_contents()[0] # only one /Contents now!
>>> cont = doc.xref_stream(xref)
>>> # this has also reformatted the PDF commands
>>> # method 2: extract concatenated contents
>>> cont = page.read_contents()
>>> # the /Contents source itself is unmodified
清理函数Page.clean_contents()
不仅仅是粘合contents
对象:它还会更正和优化页面的 PDF 操作符语法,并消除与页面对象定义的任何不一致之处。
如何访问 PDF 目录
这是 PDF 的中心(“根”)对象。它作为到达重要其他对象的起点,并包含 PDF 的一些全局选项:
>>> import pymupdf
>>> doc=pymupdf.open("PyMuPDF.pdf")
>>> cat = doc.pdf_catalog() # get xref of the /Catalog
>>> print(doc.xref_object(cat)) # print object definition
<<
/Type/Catalog % object type
/Pages 3593 0 R % points to page tree
/OpenAction 225 0 R % action to perform on open
/Names 3832 0 R % points to global names tree
/PageMode /UseOutlines % initially show the TOC
/PageLabels<</Nums[0<</S/D>>2<</S/r>>8<</S/D>>]>> % labels given to pages
/Outlines 3835 0 R % points to outline tree
>>
注意
缩进、换行和注释仅用于澄清目的,通常不会显示。有关 PDF 目录的更多信息,请参见 Adobe PDF References 第 71 页的第 7.7.2 节。
如何访问 PDF 文件尾页
PDF 文件的尾页是位于文件末尾的dictionary
。它包含特殊对象和指向重要其他信息的指针。参见 Adobe PDF References 第 42 页。以下是概述:
Key | Type | Value |
---|---|---|
Size | int | 交叉引用表中条目数 + 1。 |
Prev | int | 指向前一个xref 部分的偏移量(指示增量更新)。 |
Root | dictionary | (间接)指向目录的指针。请参阅上一节。 |
Encrypt | dictionary | 指向加密对象的指针(仅加密文件)。 |
Info | dictionary | (间接)指向信息(元数据)的指针。 |
ID | array | 由两个字节字符串组成的文件标识符。 |
XRefStm | int | 交叉引用流的偏移量。参见 Adobe PDF References 第 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 References 第 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字典
键。
-
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 字典可以按层次嵌套。在以下页面对象定义中,Font 和
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
: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。换句话说,xrefs 只能直接修改,而不能通过引用它们的其他对象隐式地修改它们:
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上的许可信息或联系 Artifex Software Inc.,39 Mesa Street,Suite 108A,San Francisco CA 94129,United States。
此文档涵盖了所有版本直至 1.24.4。
如何遍历xref
表
PDF 的xref
表是文件中定义的所有对象的列表。此表可能很容易包含许多条目 - 例如,手册 Adobe PDF References 有 127,000 个对象。表条目“0”已保留,不得触摸。以下脚本循环遍历xref
表并打印每个对象的定义:
>>> xreflen = doc.xref_length() # length of objects table
>>> for xref in range(1, xreflen): # skip item 0!
print("")
print("object %i (stream: %s)" % (xref, doc.xref_is_stream(xref)))
print(doc.xref_object(xref, compressed=False))
这将产生以下输出:
object 1 (stream: False)
<<
/ModDate (D:20170314122233-04'00')
/PXCViewerInfo (PDF-XChange Viewer;2.5.312.1;Feb 9 2015;12:00:06;D:20170314122233-04'00')
>>
object 2 (stream: False)
<<
/Type /Catalog
/Pages 3 0 R
>>
object 3 (stream: False)
<<
/Kids [ 4 0 R 5 0 R ]
/Type /Pages
/Count 2
>>
object 4 (stream: False)
<<
/Type /Page
/Annots [ 6 0 R ]
/Parent 3 0 R
/Contents 7 0 R
/MediaBox [ 0 0 595 842 ]
/Resources 8 0 R
>>
...
object 7 (stream: True)
<<
/Length 494
/Filter /FlateDecode
>>
...
PDF 对象定义是普通的 ASCII 字符串。
如何处理对象流
一些对象类型除了其对象定义之外还包含其他数据。例如,图像、字体、嵌入文件或描述页面外观的命令。
这些类型的对象称为“流对象”。PyMuPDF 允许通过方法Document.xref_stream()
读取对象的流,其中对象的xref
作为参数。还可以使用Document.update_stream()
写回修改后的流的版本。
假设以下代码片段想要出于任何原因读取 PDF 的所有流:
>>> xreflen = doc.xref_length() # number of objects in file
>>> for xref in range(1, xreflen): # skip item 0!
if stream := doc.xref_stream(xref):
# do something with it (it is a bytes object or None)
# e.g. just write it back:
doc.update_stream(xref, stream)
Document.xref_stream()
自动返回解压缩为字节对象的流 - 如果有利,Document.update_stream()
会自动压缩它。
如何处理页面内容
PDF 页面可以有零个或多个contents
对象。这些是描述页面上出现的内容的流对象(如文本和图像)。它们是用特殊的迷你语言编写的,例如在 Adobe PDF References 的第 643 页的第“附录 A - 运算符摘要”中描述。
每个 PDF 阅读器应用程序必须能够解释内容语法,以重现页面的预期外观。
如果提供了多个contents
对象,则必须按照指定顺序解释它们,就像它们是几个对象的串联一样。
有多个contents
对象的技术优势:
-
添加新的
contents
对象比维护一个单一的大对象要简单得多,速度也更快(因为每次更改都需要读取、解压缩、修改、重新压缩和重写)。 -
在处理增量更新时,修改过的大
contents
对象会使更新增量膨胀,因此很容易抵消增量保存的效率。
例如,PyMuPDF 在Page.insert_image()
,Page.show_pdf_page()
方法和 Shape 方法中添加了新的小contents
对象。
然而,在某些情况下,单个contents
对象也是有利的:它比多个较小的对象更易于解释,且更易于压缩。
以下是组合页面多个内容的两种方法:
>>> # method 1: use the MuPDF clean function
>>> page.clean_contents() # cleans and combines multiple Contents
>>> xref = page.get_contents()[0] # only one /Contents now!
>>> cont = doc.xref_stream(xref)
>>> # this has also reformatted the PDF commands
>>> # method 2: extract concatenated contents
>>> cont = page.read_contents()
>>> # the /Contents source itself is unmodified
清理函数Page.clean_contents()
不仅粘合contents
对象,还会修正和优化页面的 PDF 操作符语法,并删除页面对象定义的任何不一致之处。
如何访问 PDF 目录
这是 PDF 的中心(“根”)对象。它作为到达重要其他对象的起点,并且还包含 PDF 的一些全局选项:
>>> import pymupdf
>>> doc=pymupdf.open("PyMuPDF.pdf")
>>> cat = doc.pdf_catalog() # get xref of the /Catalog
>>> print(doc.xref_object(cat)) # print object definition
<<
/Type/Catalog % object type
/Pages 3593 0 R % points to page tree
/OpenAction 225 0 R % action to perform on open
/Names 3832 0 R % points to global names tree
/PageMode /UseOutlines % initially show the TOC
/PageLabels<</Nums[0<</S/D>>2<</S/r>>8<</S/D>>]>> % labels given to pages
/Outlines 3835 0 R % points to outline tree
>>
注意
缩进、换行和注释仅为澄清目的而插入,通常不会显示。有关 PDF 目录的更多信息,请参见 Adobe PDF References 第 71 页的 7.7.2 节。
如何访问 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 更新 由其他软件创建的注释。
-
使用 PyMuPDF 创建 注释,稍后用其他软件更改它。
在这两种情况下,可能会发生意外更改,如不同的注释图标或文本字体,填充颜色或线型消失,线段末端符号大小变化甚至消失等等。
原因
每个 PDF 维护应用程序处理注释维护的方式不同。某些注释类型可能不受支持,或者不完全支持,或者某些细节可能以与另一个应用程序不同的方式处理。没有标准。
几乎每个 PDF 应用程序都配有自己的图标(文件附件、便笺和印章)以及自己支持的文本字体集。例如:
-
(Py-)MuPDF 仅支持这 5 种基本字体的‘FreeText’注释:Helvetica、Times-Roman、Courier、ZapfDingbats 和 Symbol - 没有斜体/粗体变体。当更改由其他应用程序创建的‘FreeText’注释时,其字体可能不会被识别或接受,会被替换为 Helvetica。
-
PyMuPDF 支持所有 PDF 文本标记(高亮、下划线、删除线、波浪线),但这些类型无法在 Adobe Acrobat Reader 中更新。
大多数情况下,对线段破折号的支持也有限,这会导致现有的虚线被直线替换。例如:
- PyMuPDF 完全支持所有线段破折号形式,而其他查看器仅接受有限的子集。
解决方案
不幸的是,在大多数情况下,你无能为力。
-
使用相同软件来创建和更改注释。
-
当使用 PyMuPDF 更改“外部”注释时,尽量避免使用
Annot.update()
。以下方法无需此操作,可以保持原始外观:
Annot.set_rect()
(位置变更)Annot.set_flags()
(注释行为)Annot.set_info()
(元信息,除内容之外的更改)Annot.set_popup()
(创建弹出窗口或更改其矩形)Annot.set_optional_content()
(添加/移除可选内容信息的引用)Annot.set_open()
Annot.update_file()
(文件附件更改)
丢失或无法读取的提取文本
文本提取经常不按预期工作:文本可能丢失,或者不会出现在屏幕上可见的阅读顺序中,或者包含乱码字符(如问号或“TOFU”符号)等。这可能由多种不同问题引起。
问题:未提取到任何文本
你的 PDF 查看器显示文本,但无法用光标选择它,文本提取结果为空。
原因
-
你可能在 PDF 页面中看到嵌入的图像(例如扫描的 PDF)。
-
PDF 创建者未使用字体,而是通过绘制使用小线条和曲线来模拟文本。例如,一个大写的“D”可能是通过一条“|”和一个左开放的半圆绘制,一个“o”可能是通过一个椭圆绘制,依此类推。
解决方案
使用 OCR 软件如 OCRmyPDF 在可见页面下方插入隐藏文本层。生成的 PDF 应如预期般运行。
问题:无法阅读的文本
文本提取未按可读顺序交付文本,重复某些文本或以其他方式混乱。
原因
-
单个字符可读取(无“<?>”符号),但文件中的文本编码顺序与阅读顺序不符合。背后的动机可能是技术性的或用于保护数据免受未经授权的复制。
-
许多“<?>”符号出现,表明 MuPDF 无法解释这些字符。字体可能确实不受 MuPDF 支持,或者 PDF 创建者可能使用了一种显示可读文本但有意混淆对应 Unicode 字符的字体。
解决方案
-
使用保留布局的文本提取:
python -m fitz gettext file.pdf
。 -
如果其他文本提取工具也不起作用,那么唯一的解决方案还是对页面进行 OCR 处理。
您对本页面有什么反馈吗?
本软件按原样提供,不附带任何明示或暗示的担保。本软件根据许可证分发,未经许可不得复制、修改或分发。请参阅许可信息,详情请访问 artifex.com 或联系美国加利福尼亚州旧金山 Mesa 街 39 号 108A 套房的 Artifex Software Inc. 以获取更多信息。
此文档涵盖所有版本,直至 1.24.4。
如何动态清理损坏的 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 更新由其他软件创建的注释。
-
使用 PyMuPDF 创建 注释,然后使用其他软件更改它。
在这两种情况下,您可能会遇到意外的变化,比如不同的注释图标或文本字体,填充颜色或线条虚线消失,线条末尾符号的大小已更改或甚至也已消失等。
原因
每个 PDF 维护应用程序对注释维护的处理方式各不相同。某些注释类型可能不受支持,或者支持不完全,或者某些细节可能与其他应用程序处理方式不同。没有标准。
几乎每个 PDF 应用程序还配有自己的图标(文件附件、便笺和图章)和一组支持的文本字体。例如:
-
(Py-)MuPDF 仅支持‘FreeText’注释的这 5 种基本字体:Helvetica,Times-Roman,Courier,ZapfDingbats 和 Symbol – 没有斜体/加粗变体。当更改由其他应用程序创建的‘FreeText’注释时,其字体可能不会被识别或接受,并被 Helvetica 替换。
-
PyMuPDF 支持所有 PDF 文本标记(高亮、下划线、删除线、波浪线),但这些类型无法与 Adobe Acrobat Reader 一起更新。
在大多数情况下,线条虚线的支持也是有限的,这导致现有虚线被直线替换。例如:
- PyMuPDF 完全支持所有线条虚线形式,而其他查看器只接受有限的子集。
解决方案
不幸的是,在大多数情况下,您无法做太多事情。
-
建议您使用相同的软件创建和更改注释。
-
当使用 PyMuPDF 更改一个“外来”注释时,请尽量避免使用
Annot.update()
。以下方法可以在没有它的情况下使用,以保持原始外观:
Annot.set_rect()
(位置更改)Annot.set_flags()
(注释行为)Annot.set_info()
(元信息,除了内容更改)Annot.set_popup()
(创建弹出窗口或更改其矩形)Annot.set_optional_content()
(添加/删除可选内容信息的引用)Annot.set_open()
Annot.update_file()
(文件附件更改)
问题
有两种情况:
-
使用 PyMuPDF 更新由其他软件创建的注释。
-
使用 PyMuPDF 创建注释,然后使用其他软件更改它。
在这两种情况下,您可能会遇到意外的更改,比如不同的注释图标或文本字体,填充颜色或线条虚线已经消失,线段终止符号改变了大小甚至也消失了,等等。
原因
每个 PDF 维护应用程序都以不同的方式处理注释维护。某些注释类型可能不受支持,或者支持不完全,或者某些细节可能以与另一个应用程序不同的方式处理。没有标准。
几乎每个 PDF 应用程序都配备了自己的图标(文件附件、便笺和印章)和自己支持的文本字体。例如:
-
(Py-)MuPDF 仅支持这 5 种基本字体用于“FreeText”注释:Helvetica、Times-Roman、Courier、ZapfDingbats 和 Symbol – 没有斜体/粗体变体。当更改其他应用程序创建的“FreeText”注释时,其字体可能无法识别或接受,并被替换为 Helvetica。
-
PyMuPDF 支持所有 PDF 文本标记(高亮、下划线、删除线、波浪线),但这些类型无法使用 Adobe Acrobat Reader 更新。
大多数情况下,线条虚线也存在有限的支持,这会导致现有的虚线被直线替换。例如:
- PyMuPDF 完全支持所有线条虚线形式,而其他查看器只接受有限的子集。
解决方案
不幸的是,在大多数情况下,您无法做太多事情。
-
建议您使用相同的软件创建和更改注释。
-
当使用 PyMuPDF 更改一个“外来”的注释时,请尽量避免使用
Annot.update()
。以下方法可以在没有它的情况下使用,以便保持原始外观:
Annot.set_rect()
(位置更改)Annot.set_flags()
(注释行为)Annot.set_info()
(元信息,不包括内容的更改)Annot.set_popup()
(创建弹出窗口或更改其位置)Annot.set_optional_content()
(添加/删除可选内容信息)Annot.set_open()
Annot.update_file()
(文件附件更改)
未提取到或无法读取的文本
文本提取经常不按您期望的方式工作:可能缺少文本,或者在屏幕上可见的阅读顺序中没有出现,或者包含乱码字符(如?或“TOFU”符号)等。这可能由多种不同的问题引起。
问题:未提取到文本
您的 PDF 查看器显示文本,但无法用鼠标选择它,文本提取结果为空。
原因
-
可能您正在查看嵌入在 PDF 页面中的图像(例如扫描的 PDF)。
-
PDF 创建者未使用字体,而是通过绘制使用小线条和曲线来模拟文本。例如,一个大写的“D”可以通过一条竖线“|”和一个左开的半圆来绘制,“o”可以通过一个椭圆来绘制,等等。
解决方案
使用像OCRmyPDF这样的 OCR 软件,在可见页面下方插入一个隐藏文本层。生成的 PDF 应该按预期方式工作。
问题:不可读文本
文本提取未按可读顺序提供文本,重复了一些文本,或者其他情况下乱码。
原因
-
单个字符可直接阅读(没有“<?>”符号),但文件中编码的顺序与阅读顺序不同。这背后的动机可能是技术性的或为了保护数据免受未经授权的复制。
-
许多“<?>”符号出现,表明 MuPDF 无法解释这些字符。字体可能确实不受 MuPDF 支持,或者 PDF 创建者可能使用了一种显示可读文本但故意混淆原始对应 Unicode 字符的字体。
解决方案
-
使用保留布局的文本提取:
python -m fitz gettext file.pdf
。 -
如果其他文本提取工具也不起作用,那么唯一的解决方案再次是对页面进行 OCR。
对本页面有任何反馈吗?
本软件按原样提供,不提供任何明示或暗示的担保。本软件根据许可证分发,并且未经明确授权不得复制、修改或分发。有关详细信息,请参阅许可信息,网址为artifex.com,或联系美国旧金山 94129 号,Mesa 街 39 号 108A 套房的 Artifex Software Inc.。
本文档覆盖所有版本,直至 1.24.4。
问题:未提取任何文本
您的 PDF 查看器显示文本,但您不能用光标选择它,且文本提取无效。
原因
-
您可能在 PDF 页面中嵌入了图像(例如扫描过的 PDF)。
-
PDF 创建者未使用任何字体,而是通过绘制使用小线条和曲线来模拟文本。例如,大写字母“D”可以由一条竖线“|”和一个左开弧组成,“o”可以由一个椭圆组成,依此类推。
解决方案
使用 OCR 软件像OCRmyPDF在可见页面下插入一个隐藏的文本层。生成的 PDF 应按预期工作。
问题:无法阅读的文本
文本提取未按可读顺序提供文本,复制了部分文本,或者内容混乱。
原因
-
这些单个字符可读性良好(无“<?>”符号),但文件中编码的文本顺序与阅读顺序不同。其动机可能是技术性的或为了保护数据免受未经授权的复制。
-
出现许多“<?>”符号,表明 MuPDF 无法解释这些字符。该字体可能确实不受 MuPDF 支持,或者 PDF 创建者可能使用了一种显示可读文本但故意混淆原始对应 Unicode 字符的字体。
解决方案
-
使用保留布局的文本提取:
python -m fitz gettext file.pdf
。 -
如果其他文本提取工具也无法正常工作,则唯一的解决方案是再次对页面进行 OCR 处理。
对本页有任何反馈意见吗?
本软件按原样提供,不提供任何明示或暗示的担保。本软件根据许可证分发,并且未经明确授权不得复制、修改或分发。有关详细信息,请参阅许可信息,网址为artifex.com,或联系美国旧金山 94129 号,Mesa 街 39 号 108A 套房的 Artifex Software Inc.。
本文档覆盖所有版本,直至 1.24.4。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了