当前位置: 首页 > news >正文

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

文章目录

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

0 引言

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

  • PDF图片去水印
  • 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 firstpix = 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 Imagedoc = 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 pagespage = doc[page_index]  # get the pageimage_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 listxref = img[0]  # get the XREF of the imageimg_info = page.get_image_info(xrefs=xref)[0]imgdict = doc.extract_image(xref)imgdata = imgdict["image"]  # image datapil_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 pymupdfdoc = pymupdf.open("output.pdf")  # open a documentfor page_index in range(len(doc)):  # iterate over pdf pagespage = doc[page_index]  # get the pageimage_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 listxref = img[0]  # get the XREF of the imageimg_info = page.get_image_info(xrefs=xref)[0]imgdict = doc.extract_image(xref)imgdata = imgdict["image"]  # image datapix_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 Imagedoc = 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 pagespage = doc[page_index]  # get the pageimage_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 listxref = img[0]  # get the XREF of the imageimgdict = doc.extract_image(xref)imgdata = imgdict["image"]  # image datapil_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 图片存储格式与元数据解析 ↩︎

http://www.dtcms.com/a/277003.html

相关文章:

  • RHCIA第二次综合实验:OSPF
  • 电阻抗成像肺功能测试数据分析与直方图生成
  • 攻防世界——Web题 very_easy_sql
  • Rust 模块系统:控制作用域与私有性
  • python 虚拟环境 Anaconda Miniconda
  • 大模型的Temperature、Top-P、Top-K、Greedy Search、Beem Search
  • jeepay开源项目开发中金支付如何像其他支付渠道对接那样简单集成,集成服务商模式,极简集成工具。
  • AI驱动的软件工程(中):文档驱动的编码与执行
  • 深入解析 ArrayList
  • XGBoost三部曲:XGBoost原理
  • Docker一键安装中间件(RocketMq、Nginx、MySql、Minio、Jenkins、Redis)脚步
  • Transformer 小记(一):深入理解 Transformer 中的位置关系
  • 【PTA数据结构 | C语言版】字符串截取子串操作
  • ABP VNext + 多级缓存架构:本地 + Redis + CDN
  • ref 和 reactive
  • EWSGAN:自动搜索高性能的GAN生成器架构
  • LeetCode 1156.单字符重复子串的最大长度
  • 维基艺术图片: 数据标注 (2)
  • C语言基础教程(002):变量介绍
  • 一文读懂现代卷积神经网络—使用块的网络(VGG)
  • 基于Prompt结构的语校解析:3H日本语学校信息建模实录(4/500)
  • 08.如何正确关闭文件
  • 数智管理学(三十三)
  • 归并排序递归法和非递归法的简单简单介绍
  • Gin框架统一响应与中间件机制学习笔记
  • DH(Denavit–Hartenberg)矩阵
  • KL散度:信息差异的量化标尺 | 从概率分布对齐到模型优化的核心度量
  • 使用QtTest
  • 反激变换器设计全流程(一)——电路拓扑及工作流程
  • Chrome v109.0.5414.168 绿色便携版 Windows 7/2012R2 最终版 下载