图像#
如何从文档页面创建图像#
这个小脚本将接受一个文档文件名,并从其每个页面生成一个 PNG 文件。
文档可以是任何支持的类型。
该脚本作为命令行工具运行,期望文件名作为参数提供。生成的图像文件(每页 1 个)存储在脚本所在的目录中。
import sys, pymupdf # import the bindings
fname = sys.argv[1] # get filename from command line
doc = pymupdf.open(fname) # open document
for page in doc: # iterate through the pages
pix = page.get_pixmap() # render page to an image
pix.save("page-%i.png" % page.number) # store image as a PNG
脚本目录现在将包含名为 page-0.png, page-1.png 等的 PNG 图像文件。图片具有其页面的尺寸,宽度和高度四舍五入为整数,例如 A4 竖版页面的 595 x 842 像素。它们在 x 和 y 维度上将具有 96 dpi 的分辨率,并且没有透明度。您可以更改所有这些 – 关于如何做到这一点,请阅读下一节。
如何提高图像分辨率#
文档页面的图像由 Pixmap 表示,创建 pixmap 的最简单方法是通过方法Page.get_pixmap()
。
此方法有许多选项可以影响结果。其中最重要的是 Matrix,它可以让你缩放、旋转、扭曲或镜像输出。
Page.get_pixmap()
默认将使用 Identity 矩阵,它不做任何事情。
接下来,我们对每个维度应用 2 倍的缩放因子,这将为我们生成一个分辨率提高四倍(尺寸也大约是四倍)的图像。
zoom_x = 2.0 # horizontal zoom
zoom_y = 2.0 # vertical zoom
mat = pymupdf.Matrix(zoom_x, zoom_y) # zoom factor 2 in each dimension
pix = page.get_pixmap(matrix=mat) # use 'mat' instead of the identity matrix
自版本 1.19.2 起,有一种更直接的方法来设置分辨率:可以使用参数 "dpi"
(每英寸点数)代替 "matrix"
。要创建页面的 300 dpi 图像,请指定 pix = page.get_pixmap(dpi=300)
。除了符号简洁之外,这种方法还有一个额外的优点,即 dpi 值会随图像文件一起保存 – 这在使用 Matrix 符号时不会自动发生。
如何创建部分 Pixmap (剪辑)#
你并不总是需要或想要页面的完整图像。例如,当你在 GUI 中显示图像并希望用页面的缩放部分填充相应的窗口时,就是这种情况。
假设你的 GUI 窗口有足够的空间显示一个完整的文档页面,但你现在想用页面的右下四分之一来填充这个空间,从而使用四倍更好的分辨率。
为此,定义一个等于你希望在 GUI 中显示的区域的矩形,并将其称为“剪辑”。在 PyMuPDF 中构造矩形的一种方法是提供两个对角相对的角,这正是我们在这里所做的。

mat = pymupdf.Matrix(2, 2) # zoom factor 2 in each direction
rect = page.rect # the page rectangle
mp = (rect.tl + rect.br) / 2 # its middle point, becomes top-left of clip
clip = pymupdf.Rect(mp, rect.br) # the area we want
pix = page.get_pixmap(matrix=mat, clip=clip)
在上面,我们通过指定两个对角相对的点来构造 clip:页面矩形的中心点 mp 和其右下角 rect.br。
如何将剪辑缩放到 GUI 窗口#
另请阅读上一节。这次我们希望为剪辑计算缩放因子,使其图像最适合给定的 GUI 窗口。这意味着图像的宽度或高度(或两者)将等于窗口尺寸。对于以下代码片段,你需要提供接收页面剪辑矩形的 GUI 窗口的 WIDTH 和 HEIGHT。
# WIDTH: width of the GUI window
# HEIGHT: height of the GUI window
# clip: a subrectangle of the document page
# compare width/height ratios of image and window
if clip.width / clip.height < WIDTH / HEIGHT:
# clip is narrower: zoom to window HEIGHT
zoom = HEIGHT / clip.height
else: # clip is broader: zoom to window WIDTH
zoom = WIDTH / clip.width
mat = pymupdf.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat, clip=clip)
反过来,现在假设你知道缩放因子,并且需要计算合适的剪辑。
在这种情况下,我们有 zoom = HEIGHT/clip.height = WIDTH/clip.width
,所以我们必须设置 clip.height = HEIGHT/zoom
和 clip.width = WIDTH/zoom
。选择页面上剪辑的左上角点 tl
来计算正确的 pixmap。
width = WIDTH / zoom
height = HEIGHT / zoom
clip = pymupdf.Rect(tl, tl.x + width, tl.y + height)
# ensure we still are inside the page
clip &= page.rect
mat = pymupdf.Matrix(zoom, zoom)
pix = pymupdf.Pixmap(matrix=mat, clip=clip)
如何创建或抑制注解图像#
通常,页面的 pixmap 也显示页面的注解。有时,这可能不是期望的。
要在渲染的页面上抑制注解图像,只需在 Page.get_pixmap()
中指定 annots=False
。
你也可以单独渲染注解:它们有自己的 Annot.get_pixmap()
方法。生成的 pixmap 具有与注解矩形相同的尺寸。
如何提取图像:非 PDF 文档#
与前几节不同,本节讨论提取文档中包含的图像,以便它们可以作为一页或多页的一部分显示。
如果你想以文件形式或作为内存区域重新创建原始图像,你基本上有两种选择。
将你的文档转换为 PDF,然后使用仅适用于 PDF 的提取方法之一。这个代码片段将把文档转换为 PDF。
>>> pdfbytes = doc.convert_to_pdf() # this a bytes object >>> pdf = pymupdf.open("pdf", pdfbytes) # open it as a PDF document >>> # now use 'pdf' like any PDF document
使用带有“dict”参数的
Page.get_text()
。这适用于所有文档类型。它将提取页面上显示的所有文本和图像,格式化为 Python 字典。每个图像将出现在一个图像块中,包含元信息和二进制图像数据。有关字典结构的详细信息,请参见 TextPage。该方法对 PDF 文件同样有效。这将创建一个页面上显示的所有图像的列表。>>> d = page.get_text("dict") >>> blocks = d["blocks"] # the list of block dictionaries >>> imgblocks = [b for b in blocks if b["type"] == 1] >>> pprint(imgblocks[0]) {'bbox': (100.0, 135.8769989013672, 300.0, 364.1230163574219), 'bpc': 8, 'colorspace': 3, 'ext': 'jpeg', 'height': 501, 'image': b'\xff\xd8\xff\xe0\x00\x10JFIF\...', # CAUTION: LARGE! 'size': 80518, 'transform': (200.0, 0.0, -0.0, 228.2460174560547, 100.0, 135.8769989013672), 'type': 1, 'width': 439, 'xres': 96, 'yres': 96}
如何提取图像:PDF 文档#
就像 PDF 中的任何其他“对象”一样,图像通过交叉引用号(xref
,一个整数)来标识。如果你知道这个号码,你有两种方法访问图像数据。
使用指令 pix = pymupdf.Pixmap(doc, xref) 创建图像的 Pixmap。此方法非常快(单位是微秒)。pixmap 的属性(宽度、高度等)将反映图像的属性。在这种情况下,无法判断嵌入的原始图像是什么格式。
使用 img = doc.extract_image(xref) 提取图像。这是一个字典,包含二进制图像数据作为 img[“image”]。还提供了许多元数据 – 大部分与你在图像的 pixmap 中找到的相同。主要区别在于字符串 img[“ext”],它指定了图像格式:除了“png”之外,还可能出现“jpeg”、“bmp”、“tiff”等字符串。如果你想存储到磁盘,请使用此字符串作为文件扩展名。此方法的执行速度应与语句 pix = pymupdf.Pixmap(doc, xref);pix.tobytes() 的总速度进行比较。如果嵌入图像是 PNG 格式,
Document.extract_image()
的速度大致相同(并且二进制图像数据相同)。否则,此方法快数千倍,并且图像数据小得多。
问题仍然是:“我怎么知道那些图像的‘xref’号?”。对此有两个答案。
“检查页面对象:” 遍历
Page.get_images()
的项目。它是一个列表的列表,其项目看起来像 [xref, smask, …],包含图像的xref
。然后可以将此xref
用于上述方法之一。将此方法用于有效(未损坏)的文档。但是请注意,同一个图像可能被多次引用(由不同的页面),因此你可能需要提供一种机制来避免多次提取。“无需知道:” 遍历文档中所有 xrefs 的列表,并为每个 xref 执行一次
Document.extract_image()
。如果返回的字典为空,则继续 – 这个xref
不是图像。如果 PDF 损坏(页面不可用),请使用此方法。请注意,PDF 通常包含用于定义其他图像透明度的“伪图像”(“模板遮罩”)。你可能需要提供逻辑来排除这些图像的提取。另请查看下一节。
对于这两种提取方法,都有现成的通用脚本可用。
extract-from-pages.py 逐页提取图像

以及 extract-from-xref.py 按 xref 表提取图像

如何处理图像遮罩#
PDF 中的某些图像附带图像遮罩。在其最简单的形式中,遮罩表示作为单独图像存储的 Alpha(透明度)字节。为了重建具有遮罩的原始图像,必须使用从其遮罩中提取的透明度字节来“丰富”它。
图像是否具有这种遮罩可以通过 PyMuPDF 中的两种方式之一识别出来。
Document.get_page_images()
的一个项目具有通用格式(xref, smask, ...)
,其中xref
是图像的xref
,而 smask 如果为正,则是遮罩的xref
。Document.extract_image()
的(字典)结果有一个键 “smask”,如果为正,它也包含任何遮罩的xref
。
如果 smask == 0,则通过 xref
遇到的图像可以按原样处理。
要使用 PyMuPDF 恢复原始图像,必须执行如下所示的过程:

>>> pix1 = pymupdf.Pixmap(doc.extract_image(xref)["image"]) # (1) pixmap of image w/o alpha
>>> mask = pymupdf.Pixmap(doc.extract_image(smask)["image"]) # (2) mask pixmap
>>> pix = pymupdf.Pixmap(pix1, mask) # (3) copy of pix1, image mask added
步骤 (1) 创建基本图像的 pixmap。步骤 (2) 对图像遮罩执行相同的操作。步骤 (3) 添加一个 alpha 通道并填充透明度信息。
上面的脚本 extract-from-pages.py 和 extract-from-xref.py 也包含此逻辑。
如何将所有图片(或文件)合并为一个 PDF#
我们在这里展示三个脚本,它们接受一个(图像和其他)文件列表,并将它们全部放入一个 PDF 中。
方法 1:将图像作为页面插入
第一个脚本将每张图像转换为尺寸相同的 PDF 页面。结果将是一个每张图像对应一页的 PDF。它仅适用于支持的图像文件格式。
import os, pymupdf
import PySimpleGUI as psg # for showing a progress bar
doc = pymupdf.open() # PDF with the pictures
imgdir = "D:/2012_10_05" # where the pics are
imglist = os.listdir(imgdir) # list of them
imgcount = len(imglist) # pic count
for i, f in enumerate(imglist):
img = pymupdf.open(os.path.join(imgdir, f)) # open pic as document
rect = img[0].rect # pic dimension
pdfbytes = img.convert_to_pdf() # make a PDF stream
img.close() # no longer needed
imgPDF = pymupdf.open("pdf", pdfbytes) # open stream as PDF
page = doc.new_page(width = rect.width, # new page with ...
height = rect.height) # pic dimension
page.show_pdf_page(rect, imgPDF, 0) # image fills the page
psg.EasyProgressMeter("Import Images", # show our progress
i+1, imgcount)
doc.save("all-my-pics.pdf")
这将生成一个 PDF,其大小仅比所有图片的总大小略大。以下是一些性能数据。
上述脚本在我的机器上处理 149 张总大小为 514 MB 的图片(生成的 PDF 大小也大致相同)大约需要 1 分钟。

请点击此处查看更完整的源代码:它提供目录选择对话框,并跳过不支持的文件和非文件条目。
注意
我们可能已经使用 Page.insert_image()
而不是 Page.show_pdf_page()
,结果将是一个看起来相似的文件。然而,根据图像类型,它可能会存储未压缩的图像。因此,必须使用保存选项 deflate = True 来实现合理的文件大小,这会显著增加处理大量图像时的运行时。因此,此处不建议使用此替代方法。
方法 2:嵌入文件
第二个脚本嵌入任意文件 – 不仅是图像。生成的 PDF 将只有一页(空白页),这是出于技术原因所需的。要稍后再次访问嵌入文件,你需要一个能够显示和/或提取嵌入文件的合适 PDF 查看器。
import os, pymupdf
import PySimpleGUI as psg # for showing progress bar
doc = pymupdf.open() # PDF with the pictures
imgdir = "D:/2012_10_05" # where my files are
imglist = os.listdir(imgdir) # list of pictures
imgcount = len(imglist) # pic count
imglist.sort() # nicely sort them
for i, f in enumerate(imglist):
img = open(os.path.join(imgdir,f), "rb").read() # make pic stream
doc.embfile_add(img, f, filename=f, # and embed it
ufilename=f, desc=f)
psg.EasyProgressMeter("Embedding Files", # show our progress
i+1, imgcount)
page = doc.new_page() # at least 1 page is needed
doc.save("all-my-pics-embedded.pdf")

这是目前最快的方法,它也生成最小的输出文件大小。上述图片在我的机器上需要 20 秒,生成的 PDF 大小为 510 MB。请点击此处查看更完整的源代码:它提供目录选择对话框,并跳过非文件条目。
方法 3:附加文件
实现此任务的第三种方法是通过页面注解附加文件,请点击此处查看完整的源代码。
这与之前的脚本性能相似,并且生成的文件大小也相似。它将生成显示每个附加文件的“文件附件”图标的 PDF 页面。

注意
嵌入和附加这两种方法都可用于任意文件 – 不仅是图像。
注意
我们强烈建议使用强大的 PySimpleGUI 包来显示可能长时间运行的任务的进度条。它是纯 Python 实现,使用 Tkinter(无需额外的 GUI 包),并且只需要多写一行代码!
如何创建矢量图像#
从文档页面创建图像的常用方法是 Page.get_pixmap()
。Pixmap 表示光栅图像,因此你必须在创建时决定其质量(即分辨率)。之后无法更改。
PyMuPDF 还提供一种创建页面 SVG 格式(可伸缩矢量图形,用 XML 语法定义)的矢量图像的方法。SVG 图像在不同缩放级别下仍保持精确(当然,其中嵌入的任何光栅图形元素除外)。
指令 svg = page.get_svg_image(matrix=pymupdf.Identity) 返回一个 UTF-8 字符串 svg,可以将其保存为扩展名为“.svg”的文件。
如何转换图像#
作为众多功能之一,PyMuPDF 的图像转换非常简单。在许多情况下,它可以避免使用其他图形包,如 PIL/Pillow。
尽管如此,与 Pillow 的接口几乎微不足道。
输入格式 |
输出格式 |
描述 |
---|---|---|
BMP |
. |
Windows 位图 |
JPEG |
JPEG |
联合图像专家组 |
JXR |
. |
JPEG 扩展范围 |
JPX/JP2 |
. |
JPEG 2000 |
GIF |
. |
图形交换格式 |
TIFF |
. |
标签图像文件格式 |
PNG |
PNG |
便携式网络图形 |
PNM |
PNM |
便携式任意图 |
PGM |
PGM |
便携式灰度图 |
PBM |
PBM |
便携式位图 |
PPM |
PPM |
便携式像素图 |
PAM |
PAM |
便携式任意图 |
. |
PSD |
Adobe Photoshop 文档 |
. |
PS |
Adobe Postscript |
一般方案只需以下两行
pix = pymupdf.Pixmap("input.xxx") # any supported input format
pix.save("output.yyy") # any supported output format
备注
pymupdf.Pixmap(arg) 的输入参数可以是一个文件或一个包含图像的 bytes / io.BytesIO 对象。
除了输出到文件,你也可以通过 pix.tobytes(“yyy”) 创建一个 bytes 对象并传递它。
当然,输入和输出格式必须在色彩空间和透明度方面兼容。
Pixmap
类在需要调整时已包含所需功能。
注意
将 JPEG 转换为 Photoshop:
pix = pymupdf.Pixmap("myfamily.jpg")
pix.save("myfamily.psd")
注意
将 JPEG 转换为 Tkinter PhotoImage。任何 RGB / 无 Alpha 图像都完全一样。转换为任何一种便携式任意图格式(PPM, PGM 等)都可以奏效,因为所有 Tkinter 版本都支持它们。
import tkinter as tk
pix = pymupdf.Pixmap("input.jpg") # or any RGB / no-alpha image
tkimg = tk.PhotoImage(data=pix.tobytes("ppm"))
注意
将带 Alpha 通道的 PNG 转换为 Tkinter PhotoImage。这需要在进行 PPM 转换之前移除 Alpha 字节。
import tkinter as tk
pix = pymupdf.Pixmap("input.png") # may have an alpha channel
if pix.alpha: # we have an alpha channel!
pix = pymupdf.Pixmap(pix, 0) # remove it
tkimg = tk.PhotoImage(data=pix.tobytes("ppm"))
如何使用 Pixmap:拼接图像#
这展示了 pixmap 如何用于纯图形、非文档目的。脚本读取一个图像文件并创建一个新图像,该图像由原始图像的 3 * 4 个图块组成。
import pymupdf
src = pymupdf.Pixmap("img-7edges.png") # create pixmap from a picture
col = 3 # tiles per row
lin = 4 # tiles per column
tar_w = src.width * col # width of target
tar_h = src.height * lin # height of target
# create target pixmap
tar_pix = pymupdf.Pixmap(src.colorspace, (0, 0, tar_w, tar_h), src.alpha)
# now fill target with the tiles
for i in range(col):
for j in range(lin):
src.set_origin(src.width * i, src.height * j)
tar_pix.copy(src, src.irect) # copy input to new loc
tar_pix.save("tar.png")
这是输入图片

这是输出

如何使用 Pixmap:生成分形#
这是另一个 Pixmap 示例,创建了谢尔宾斯基地毯 – 一种将康托尔集推广到二维的分形。给定一个方形地毯,标记其 9 个子方形(3 乘 3),然后挖出中心的一个。以相同的方式处理剩余的八个子方形,并无限重复。最终结果是一个面积为零、分形维数约为 1.8928 的集合…
该脚本通过细化到单像素粒度,创建其近似的 PNG 图像。要提高图像精度,请更改 n(精度)的值。
import pymupdf, time
if not list(map(int, pymupdf.VersionBind.split("."))) >= [1, 14, 8]:
raise SystemExit("need PyMuPDF v1.14.8 for this script")
n = 6 # depth (precision)
d = 3**n # edge length
t0 = time.perf_counter()
ir = (0, 0, d, d) # the pixmap rectangle
pm = pymupdf.Pixmap(pymupdf.csRGB, ir, False)
pm.set_rect(pm.irect, (255,255,0)) # fill it with some background color
color = (0, 0, 255) # color to fill the punch holes
# alternatively, define a 'fill' pixmap for the punch holes
# this could be anything, e.g. some photo image ...
fill = pymupdf.Pixmap(pymupdf.csRGB, ir, False) # same size as 'pm'
fill.set_rect(fill.irect, (0, 255, 255)) # put some color in
def punch(x, y, step):
"""Recursively "punch a hole" in the central square of a pixmap.
Arguments are top-left coords and the step width.
Some alternative punching methods are commented out.
"""
s = step // 3 # the new step
# iterate through the 9 sub-squares
# the central one will be filled with the color
for i in range(3):
for j in range(3):
if i != j or i != 1: # this is not the central cube
if s >= 3: # recursing needed?
punch(x+i*s, y+j*s, s) # recurse
else: # punching alternatives are:
pm.set_rect((x+s, y+s, x+2*s, y+2*s), color) # fill with a color
#pm.copy(fill, (x+s, y+s, x+2*s, y+2*s)) # copy from fill
#pm.invert_irect((x+s, y+s, x+2*s, y+2*s)) # invert colors
return
#==============================================================================
# main program
#==============================================================================
# now start punching holes into the pixmap
punch(0, 0, d)
t1 = time.perf_counter()
pm.save("sierpinski-punch.png")
t2 = time.perf_counter()
print ("%g sec to create / fill the pixmap" % round(t1-t0,3))
print ("%g sec to save the image" % round(t2-t1,3))
结果应该看起来像这样

如何与 NumPy 交互#
这展示了如何从 numpy 数组创建 PNG 文件(比大多数其他方法快数倍)。
import numpy as np
import pymupdf
#==============================================================================
# create a fun-colored width * height PNG with pymupdf and numpy
#==============================================================================
height = 150
width = 100
bild = np.ndarray((height, width, 3), dtype=np.uint8)
for i in range(height):
for j in range(width):
# one pixel (some fun coloring)
bild[i, j] = [(i+j)%256, i%256, j%256]
samples = bytearray(bild.tostring()) # get plain pixel data from numpy array
pix = pymupdf.Pixmap(pymupdf.csRGB, width, height, samples, alpha=False)
pix.save("test.png")
如何向 PDF 页面添加图像#
有两种方法可以向 PDF 页面添加图像:Page.insert_image()
和 Page.show_pdf_page()
。这两种方法有共同之处,但也有区别。
标准 |
||
---|---|---|
可显示内容 |
图像文件、内存中的图像、pixmap |
PDF 页面 |
显示分辨率 |
图像分辨率 |
矢量化(光栅页面内容除外) |
旋转 |
0, 90, 180 或 270 度 |
任意角度 |
剪辑 |
否(仅完整图像) |
是 |
保持纵横比 |
是(默认选项) |
是(默认选项) |
透明度(水印) |
取决于图像 |
取决于页面 |
位置 / 放置 |
缩放以适应目标矩形 |
缩放以适应目标矩形 |
性能 |
自动防止重复; |
自动防止重复; |
多页图像支持 |
否 |
是 |
易用性 |
简单,直观; |
简单,直观;通过 |
Page.insert_image()
的基本代码模式。除非重新插入现有图像,否则必须提供参数 filename / stream / pixmap 中的恰好一个。
page.insert_image(
rect, # where to place the image (rect-like)
filename=None, # image in a file
stream=None, # image in memory (bytes)
pixmap=None, # image from pixmap
mask=None, # specify alpha channel separately
rotate=0, # rotate (int, multiple of 90)
xref=0, # re-use existing image
oc=0, # control visibility via OCG / OCMD
keep_proportion=True, # keep aspect ratio
overlay=True, # put in foreground
)
Page.show_pdf_page()
的基本代码模式。源 PDF 和目标 PDF 必须是不同的 Document 对象(但可以从同一个文件打开)。
page.show_pdf_page(
rect, # where to place the image (rect-like)
src, # source PDF
pno=0, # page number in source PDF
clip=None, # only display this area (rect-like)
rotate=0, # rotate (float, any value)
oc=0, # control visibility via OCG / OCMD
keep_proportion=True, # keep aspect ratio
overlay=True, # put in foreground
)
如何使用 Pixmap:检查文本可见性#
页面上的给定文本片段是否实际可见取决于多种因素:
文本没有被其他对象覆盖,但颜色可能与背景相同,例如,白色背景上的白色文本等。
文本可能被图像或矢量图形覆盖。检测这一点是一项重要能力,例如,用于揭示匿名处理不当的法律文件。
文本被创建为隐藏状态。这种技术通常被 OCR 工具用来将识别的文本存储在页面上的一个不可见层中。
以下展示了如何检测上述情况 1 或情况 2(如果覆盖对象是单色的话)。
pix = page.get_pixmap(dpi=150) # make page image with a decent resolution
# the following matrix transforms page to pixmap coordinates
mat = page.rect.torect(pix.irect)
# search for some string "needle"
rlist = page.search_for("needle")
# check the visibility for each hit rectangle
for rect in rlist:
if pix.color_topusage(clip=rect * mat)[0] > 0.95:
print("'needle' is invisible here:", rect)
方法 Pixmap.color_topusage()
返回一个元组 (ratio, pixel)
,其中 0 < ratio <= 1,而 pixel 是颜色的像素值。请注意,我们只创建一次 pixmap。如果存在多个命中矩形,这可以节省大量处理时间。
上述代码的逻辑是:如果针的矩形是(“几乎”:> 95%)单色,则文本不可见。可见文本的典型结果是返回背景颜色(多数是白色)和比例在 0.7 到 0.8 之间,例如 (0.685, b'xffxffxff')
。