PyMuPDF 1.24.4 中文文档(五)(2)

简介: PyMuPDF 1.24.4 中文文档(五)

PyMuPDF 1.24.4 中文文档(五)(1)https://developer.aliyun.com/article/1559436


多进程

原文: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.readthedocs.io/en/latest/recipes-ocr.html

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 文档页面,请按照以下步骤操作:

  1. 确定是否需要/有利于使用 OCR。可以使用多个标准来做出此决定,例如:
  • 页面完全被图像覆盖

  • 页面上不存在文本

  • 数千个小型矢量图形(表示模拟文本)
  1. OCR 该页面并使用类似tp = page.get_textpage_ocr(...)的指令将结果存储在 TextPage 对象中。
  2. 在所有后续文本提取和搜索中引用生成的 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,请按以下步骤操作:

  1. 确定是否需要或者说 OCR 是否有益。可以根据一些标准做出决定,例如:
  • 页面完全被图像覆盖。

  • 页面上不存在文本

  • 数千个小型矢量图形(表示模拟文本)
  1. OCR 页面并使用类似tp = page.get_textpage_ocr(...)的指令将结果存储在 TextPage 对象中。
  2. 在所有后续的文本提取和搜索中,通过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 1.24.4 中文文档(五)(3)https://developer.aliyun.com/article/1559438

相关文章
|
2天前
|
存储 XML 编解码
PyMuPDF 1.24.4 中文文档(三)(2)
PyMuPDF 1.24.4 中文文档(三)
10 0
PyMuPDF 1.24.4 中文文档(三)(2)
|
2天前
|
JSON API 数据格式
PyMuPDF 1.24.4 中文文档(四)(5)
PyMuPDF 1.24.4 中文文档(四)
8 0
|
2天前
|
XML 存储 数据安全/隐私保护
PyMuPDF 1.24.4 中文文档(五)(4)
PyMuPDF 1.24.4 中文文档(五)
9 0
|
2天前
|
安全 API 数据安全/隐私保护
PyMuPDF 1.24.4 中文文档(一)(3)
PyMuPDF 1.24.4 中文文档(一)
12 2
|
2天前
|
Python
PyMuPDF 1.24.4 中文文档(三)(5)
PyMuPDF 1.24.4 中文文档(三)
8 0
PyMuPDF 1.24.4 中文文档(三)(5)
|
2天前
|
存储 机器学习/深度学习 XML
PyMuPDF 1.24.4 中文文档(二)(3)
PyMuPDF 1.24.4 中文文档(二)
12 0
PyMuPDF 1.24.4 中文文档(二)(3)
|
2天前
|
文字识别 开发工具 C++
PyMuPDF 1.24.4 中文文档(一)(2)
PyMuPDF 1.24.4 中文文档(一)
9 0
|
2天前
|
存储 数据安全/隐私保护
PyMuPDF 1.24.4 中文文档(六)(3)
PyMuPDF 1.24.4 中文文档(六)
7 0
|
2天前
|
存储 文字识别 Python
PyMuPDF 1.24.4 中文文档(五)(5)
PyMuPDF 1.24.4 中文文档(五)
9 0
|
2天前
|
存储 编解码 算法
PyMuPDF 1.24.4 中文文档(七)(4)
PyMuPDF 1.24.4 中文文档(七)
8 0