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

文章目录

    • [0 引言](#0 引言)
    • [1 解决思路与流程](#1 解决思路与流程)
    • [2 完整代码与测试效果](#2 完整代码与测试效果)
    • 参考

0 引言

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

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

1 解决思路与流程

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

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

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

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

python 复制代码
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)。

python 复制代码
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](#2)。我们肯定是选用后者,这里只是对比一下。

python 复制代码
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](#3)

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

python 复制代码
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取值一致。

python 复制代码
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),该示例的完整代码如下

python 复制代码
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更大了,这不行。

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

python 复制代码
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)替换为

python 复制代码
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("虚拟"图像),占用内存可忽略不计。

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

经尝试,发现只要在保存时设置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 完整代码与测试效果

示例最终完整代码如下:

python 复制代码
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中示例

参考


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

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

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

相关推荐
人月神话-Lee7 小时前
【图像处理】图像导出与工业级压缩策略——从像素到文件的最后一公里
图像处理·人工智能·ios·ai编程·swift
phoenix@Capricornus15 小时前
频域滤波是线性运算的证明
图像处理
wtsolutions16 小时前
QMT 知识库 XtQuant知识库 使用文档 pdf
pdf·知识库·文档·qmt
埃科光电17 小时前
埃科光电2.5D成像系统,破解精密制造微缺陷检测难题
图像处理·计算机视觉·制造·相机·机器视觉
ComputerInBook17 小时前
OpenCV图像处理——透视变换
图像处理·人工智能·opencv·透视变换
weixin_4684668519 小时前
目标识别算法落地实战:从选型到部署的全流程指南
图像处理·人工智能·python·算法·目标检测·机器视觉·目标识别
海盗123419 小时前
C#中PDF操作-QuestPDF介绍和使用教程
pdf·c#
半月夏微凉19 小时前
win11下不能预览pdf的问题解决方法
windows·pdf
yugi98783820 小时前
基于Qt的图像处理系统
开发语言·图像处理·qt
猫猫不是喵喵.20 小时前
vue2技术栈将表单内容转为PDF并下载
pdf