【PyMuPDF】PDF图片处理过程内存优化分析

发布于:2025-07-14 ⋅ 阅读:(16) ⋅ 点赞:(0)

0 引言

在之前的文章中,我们介绍了有关PDF文件图片去水印和文字颜色加深的方法,详情见如下链接:

上述方法存在着一些不足之处,便是处理后保存的PDF文件占用内存过大,比原本文件大好几倍。这篇文章的目的就是对处理后的PDF内存大小进行优化。

1 解决思路与流程

因为我们处理的PDF主要是由每页的图片组成的,因此图片的内存大小是影响PDF体量的主要因素。我们边看代码结果边分析,首先代入必要的库

import io # 用于字节流转换
import os # 用于查看文件内存占用情况
import sys # 用于查看变量字节数
import pymupdf # 用于操作PDF
from PIL import Image # 用于图片处理

这里import pymupdf而不是之前的import fitz,两者的区别参见官方文档说明1

为了兼顾调试分析的效率,我们先随便拿一个只有两页的PDF(example.pdf),该PDF由两张扫描图片组成。先读入文件,并打印内存大小。

doc = pymupdf.open("example.pdf")  # open a document
print(f"example.pdf [size]: {os.path.getsize('example.pdf') / 1024 / 1024:.2f} MB")

example.pdf [size]: 1.72 MB

接下来,我们以第一页的图片为例。get_image_info函数用于获取图片的信息,得到图像的尺寸信息,由于图像为彩色三通道,按像素矩阵保存的话需要24.80 MB的内存(1个通道值占1byte)。

page0 = doc.load_page(0)
image_list = page0.get_images()
xref0 = image_list[0][0]
img_info = page0.get_image_info(xrefs=xref0)[0]
print(f"img_array [size]: {(img_info['width'] * img_info['height'] * 3) / 1024 / 1024:.2f} MB")

img_array [size]: 24.80 MB

为了后续复用需要,我们定义函数print_size用于打印变量字节数。这里我们使用两种方法提取PDF页面内的图片,这两者方法的功能一样,但后者在速度和内层占用上都具有优势,这在官方文档中提及2。我们肯定是选用后者,这里只是对比一下。

print_size = lambda n, p: print(f"{n} [size]: {sys.getsizeof(p) / 1024 / 1024:.2f} MB")

pix = pymupdf.Pixmap(doc, xref0)  # create a Pixmap
if pix.n - pix.alpha > 3:  # CMYK: convert to RGB first
    pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
print_size('Pixmap(doc, xref0)', pix.tobytes())

imgdict = doc.extract_image(xref0)
imgdata = imgdict["image"]  # image data
print_size('imgdata(extract_image)', imgdata)

Pixmap(doc, xref0) [size]: 2.37 MB

imgdata(extract_image) [size]: 0.84 MB

我们发现extract_image函数提取出来的图像内存占用仅为0.84 MB,与我们计算的24.80 MB小得多,这是因为imgdata存储的是图像压缩后的字节信息,而不是原图像矩阵,图像压缩技术在存储上具备优势,不同的压缩格式所占用的内存大小也不同。页面图片原始的压缩标准可以通过imgdict["ext"]获取,示例中是jpeg格式(相较其他常见格式,该格式占用内存最小)。关于图片存储格式详情参见3

因为图像处理需要PIL(或OpenCV等),我们需要将提取到的图像数据转换为图像处理库支持的格式。

pil_imgdata = Image.open(io.BytesIO(imgdata))
print_size('pil_imgdata', pil_imgdata.tobytes())

pil_imgdata [size]: 24.80 MB

通过打印的内存大小我们可以得知,PIL.Image(24.80 MB)和之前计算的图像矩阵(24.80 MB)内存大小相同,猜测是因为PIL将压缩图像复原为矩阵了,方便后续的图像处理。pymupdf.Pixmap(2.37 MB)图像对象的内存占用大小比图像矩阵(24.80 MB)小,但是比imgdata(0.84 MB)大,通过调试,初步分析是因为imgdata为压缩后的字节串,而Pixmap为类对象,可能包含其他额外数据。无法直接对Pixmap对象进行图像处理操作,可能是因为其图像数据仍然是压缩格式?不知道。

图像处理后,需要替换掉原PDF中的图像,在这之前需要将PIL.Image转换为压缩格式字节流,方便放入PDF文件中。调用save函数,其中format参数是压缩标准,我们就使用原PDF中的jpeg;quality参数是压缩质量,取100会比原图像占用内存更大,取95占用内存更小(图像压缩效果再可接受范围内,以小内存为优);progressive参数是保存渐进式jpeg(该格式相比默认格式内存更小,选它);dpi参数设置越大,图像内存越大,我们和原图像dpi取值一致。

bio = io.BytesIO()
pil_imgdata.save(bio, format=imgdict["ext"], quality=95, progressive=True, dpi=(imgdict['xres'], imgdict['yres']))
print_size('pil_imgdata_save', bio)

72, 72

pil_imgdata_save [size]: 0.63 MB

很好,处理插入PDF的图像内存(0.63 MB)甚至比提取出的图像内存(0.84 MB)更小!可能是因为压缩(quality=95)的原因。

接下来的步骤是用处理后的图像替换原页面图像,采用之前文章中的方法(replace_image),该示例的完整代码如下

import io
import os
import pymupdf
from PIL import Image

doc = pymupdf.open("example.pdf")  # open a document
print(f"example.pdf [size]: {os.path.getsize('example.pdf') / 1024 / 1024:.2f} MB")

for page_index in range(len(doc)):  # iterate over pdf pages
    page = doc[page_index]  # get the page
    image_list = page.get_images()
    print(f"Find {len(image_list)} images on page {page_index}")

    for image_index, img in enumerate(image_list, start=1):  # enumerate the image list
        xref = img[0]  # get the XREF of the image
        img_info = page.get_image_info(xrefs=xref)[0]

        imgdict = doc.extract_image(xref)
        imgdata = imgdict["image"]  # image data

        pil_imgdata = Image.open(io.BytesIO(imgdata))

        # TODO:图像处理操作

        pix_imgdata = pymupdf.Pixmap(imgdata)
        bio = io.BytesIO()
        pil_imgdata.save(bio, format=imgdict["ext"], quality=95, progressive=True,
                         dpi=(pix_imgdata.xres, pix_imgdata.yres))
        # 页面图像替换
        page.replace_image(xref, stream=bio)

        print(f"Processed {image_index} images on page {page_index}")

# 保存PDF
doc.save('output.pdf')
doc.close()
print(f"output.pdf [size]: {os.path.getsize('output.pdf') / 1024 / 1024:.2f} MB")

example.pdf [size]: 1.72 MB
Find 1 images on page 0
Processed 1 images on page 0
Find 1 images on page 1
Processed 1 images on page 1
output.pdf [size]: 2.64 MB

可以看到,输出PDF比原PDF更大了,这不行。

image-20250713123732879

后来发现这是个坑啊,官方文档中的描述如上,为了证实我的猜想,写个测试脚本

import pymupdf

doc = pymupdf.open("output.pdf")  # open a document

for page_index in range(len(doc)):  # iterate over pdf pages
    page = doc[page_index]  # get the page
    image_list = page.get_images()
    print(f"Find {len(image_list)} images on page {page_index}")
    
    for image_index, img in enumerate(image_list, start=1):  # enumerate the image list
        xref = img[0]  # get the XREF of the image
        img_info = page.get_image_info(xrefs=xref)[0]

        imgdict = doc.extract_image(xref)
        imgdata = imgdict["image"]  # image data
        pix_imgdata = pymupdf.Pixmap(imgdata)
        print_size("imgdata", imgdata)

Find 2 images on page 0
imgdata [size]: 0.63 MB
imgdata [size]: 0.63 MB
Find 2 images on page 1
imgdata [size]: 0.68 MB
imgdata [size]: 0.68 MB

我还以为是将新图片从内存上替换原图片呢,没想到逻辑是把新图像放到原图像显示的位置,原图像不显示,但数据仍在页面中,占着内存呢!

经翻阅文档函数,我尝试先将原图像删除,再插入新的图像,也就是把原来的page.replace_image(xref, stream=bio)替换为

page.delete_image(xref)
page.insert_image(rect=img_info['bbox'], stream=bio)

example.pdf [size]: 1.72 MB
Find 1 images on page 0
Processed 1 images on page 0
Find 1 images on page 1
Processed 1 images on page 1
output.pdf [size]: 1.33 MB

继续查看页面图像组成

Find 3 images on page 0
imgdata [size]: 0.00 MB
imgdata [size]: 0.00 MB
imgdata [size]: 0.63 MB
Find 3 images on page 1
imgdata [size]: 0.00 MB
imgdata [size]: 0.00 MB
imgdata [size]: 0.68 MB

发现每页变成三张图像了,只不过除了新插入的图像外,其他两张都是小的透明Pixmap("虚拟"图像),占用内存可忽略不计。

对于强迫症来说受不了,于是翻阅文档,发现可以在保存时执行垃圾回收和清理,如下图所示

image-20250713132042319

经尝试,发现只要在保存时设置doc.save('output.pdf', garbage=2, clean=True)即可。同时,也解决了使用替换页面图片page.replace_image(xref, stream=bio)带来的内存增大问题。测试结果如下:

example.pdf [size]: 1.72 MB
Find 1 images on page 0
Processed 1 images on page 0
Find 1 images on page 1
Processed 1 images on page 1
output.pdf [size]: 1.32 MB

Find 1 images on page 0
imgdata [size]: 0.63 MB
Find 1 images on page 1
imgdata [size]: 0.68 MB

以上便是我解决这一问题的思路。

2 完整代码与测试效果

示例最终完整代码如下:

import io
import os
import pymupdf
from PIL import Image

doc = pymupdf.open("example.pdf")  # open a document
print(f"example.pdf [size]: {os.path.getsize('example.pdf') / 1024 / 1024:.2f} MB")

for page_index in range(len(doc)):  # iterate over pdf pages
    page = doc[page_index]  # get the page
    image_list = page.get_images()
    print(f"Find {len(image_list)} images on page {page_index}")

    for image_index, img in enumerate(image_list, start=1):  # enumerate the image list
        xref = img[0]  # get the XREF of the image
        imgdict = doc.extract_image(xref)
        imgdata = imgdict["image"]  # image data

        pil_imgdata = Image.open(io.BytesIO(imgdata))

        # TODO:图像处理操作

        bio = io.BytesIO()
        pil_imgdata.save(bio, format=imgdict["ext"], quality=95, progressive=True,
                         dpi=(imgdict['xres'], imgdict['yres']))
        page.replace_image(xref, stream=bio)

        print(f"Processed {image_index} images on page {page_index}")

# 保存PDF
doc.save('output.pdf', garbage=2, clean=True)
doc.close()
print(f"output.pdf [size]: {os.path.getsize('output.pdf') / 1024 / 1024:.2f} MB")

对前两篇文章的方法进行优化,结果如下:

  • 文章1中示例

    example.pdf [size]: 48.48 MB
    Processing: 100%|████████████████████████████████████████████████| 55/55 [04:11<00:00, 4.57s/ Page]

    PDF处理完成!输出文件: output.pdf
    output.pdf [size]: 69.04 MB

  • 文章2中示例

    image-20250713183423968

参考


  1. 关于名称fitz的说明 - PyMuPDF 1.26.0 文档 ↩︎

  2. extract_image函数说明 - PyMuPDF 1.26.0 文档 ↩︎

  3. JPEG 图片存储格式与元数据解析 ↩︎


网站公告

今日签到

点亮在社区的每一天
去签到