查看: 1172|回复: 3

PyMuPDF批量PDF转图片脚本:DPI参数、页码过滤与内存排查

[复制链接]
发表于 昨天 11:00 | 显示全部楼层 |阅读模式
PDF转图片看似简单,真正批量处理时常见问题不少:在线工具限制文件大小,阅读器导出分辨率不可控,内网环境又不适合上传文档。把转换逻辑写成 Python 脚本,调用 MuPDF 渲染引擎逐页输出位图,更适合批量、离线、可配置的场景。原文围绕 PyMuPDF 展开,重点在 DPI 控制、页码过滤、输出命名和异常排查。

在方案选择上,pdf2image 依赖 poppler,Windows 下要单独配置 PATH;pdftoppm 是系统命令行工具,做简单转换方便,但要做目录遍历、命名规则和异常处理时脚本会变复杂。PyMuPDF 通过 pip 安装,导入名是 fitz,不依赖额外系统动态库,跨平台安装相对省事。需要注意:应安装 PyMuPDF,而不是安装名为 fitz 的老库;安装完成后用 import fitz 验证导入是否成功。

环境准备建议使用独立虚拟环境,避免污染全局 Python:
  1. python -m venv pdf2img_env
  2. # Windows PowerShell 激活 Scripts 目录下的 Activate.ps1
  3. # Linux/macOS 激活:
  4. source pdf2img_env/bin/activate
  5. pip install --upgrade pip
  6. pip install PyMuPDF
复制代码

PDF 页面坐标以点为单位,1 点等于 1/72 英寸。不指定缩放时默认约 72 DPI,A4 页面大约输出 595x842 像素,屏幕浏览勉强够用,印刷或高清展示就不合适。常见做法是直接给 get_pixmap 传入 dpi 参数;如果已经计算好缩放倍率,也可以用 Matrix 指定。例如 2.0 倍约等于 144 DPI,300 DPI 对应的缩放系数是 300/72,约 4.1667。
  1. import fitz
  2. doc = fitz.open('原始文件.pdf')
  3. page = doc[0]
  4. # 直接指定 DPI
  5. pix = page.get_pixmap(dpi=300)
  6. pix.save('第1页.png')
  7. # 指定缩放矩阵,2.0 倍约等于 144 DPI
  8. zoom = fitz.Matrix(2.0, 2.0)
  9. pix = page.get_pixmap(matrix=zoom)
复制代码

下面这版脚本把输出目录、DPI、图片格式和页码范围都做成参数。页码对外从 1 开始,内部索引从 0 开始;输出文件名用 zfill(4) 补零,便于按页排序。pathlib.Path 负责路径拼接,可以减少 Windows 反斜杠转义问题。
  1. import fitz
  2. import sys
  3. from pathlib import Path
  4. def pdf_to_images(pdf_path, output_dir=None, dpi=200, img_format='png', pages=None):
  5.     pdf_path = Path(pdf_path)
  6.     if not pdf_path.exists():
  7.         raise FileNotFoundError(f'找不到PDF文件: {pdf_path}')
  8.     if output_dir is None:
  9.         output_dir = pdf_path.parent / f'images_{pdf_path.stem}'
  10.     output_dir = Path(output_dir)
  11.     output_dir.mkdir(parents=True, exist_ok=True)
  12.     doc = fitz.open(pdf_path)
  13.     total = doc.page_count
  14.     if pages is None:
  15.         page_indices = range(total)
  16.     else:
  17.         page_indices = [p for p in pages if 0 <= p < total]
  18.     if not page_indices:
  19.         print('警告:指定的页码范围无效')
  20.         return
  21.     print(f'PDF页数: {total},本次转换: {len(page_indices)} 页')
  22.     for i, page_idx in enumerate(page_indices, start=1):
  23.         page = doc[page_idx]
  24.         pix = page.get_pixmap(dpi=dpi)
  25.         suffix = str(page_idx + 1).zfill(4)
  26.         ext = img_format.lower().replace('jpg', 'jpeg')
  27.         out_file = output_dir / f'{pdf_path.stem}_{suffix}.{ext}'
  28.         pix.save(out_file)
  29.         print(f'[{i}/{len(page_indices)}] 已生成: {out_file.name}')
  30.     doc.close()
  31.     print(f'处理完成,共生成 {len(page_indices)} 张图片,保存在:{output_dir}')
  32.     return output_dir
  33. if __name__ == '__main__':
  34.     if len(sys.argv) < 2:
  35.         print('用法: python pdf2img.py <pdf文件> [--dpi 300] [--format png] [--pages 1 3 5]')
  36.         sys.exit(1)
  37.     pdf_arg = sys.argv[1]
  38.     kwargs = {}
  39.     if '--dpi' in sys.argv:
  40.         idx = sys.argv.index('--dpi')
  41.         kwargs['dpi'] = int(sys.argv[idx + 1])
  42.     if '--format' in sys.argv:
  43.         idx = sys.argv.index('--format')
  44.         kwargs['img_format'] = sys.argv[idx + 1]
  45.     if '--pages' in sys.argv:
  46.         idx = sys.argv.index('--pages')
  47.         pages = []
  48.         for v in sys.argv[idx + 1:]:
  49.             if v.isdigit():
  50.                 pages.append(int(v) - 1)
  51.             else:
  52.                 break
  53.         kwargs['pages'] = pages
  54.     try:
  55.         pdf_to_images(pdf_arg, **kwargs)
  56.     except Exception as e:
  57.         print(f'转换失败: {e}')
  58.         sys.exit(2)
复制代码

DPI 不能盲目拉高。原文给出的参考是:手机横屏浏览可用 120 DPI,电脑屏幕阅读可用 150 DPI,PPT 高清插入可用 200 DPI,A4 彩色印刷建议 300 DPI,大幅面喷绘才考虑 600 DPI。150 DPI 到 300 DPI 的清晰度差异肉眼可见,但 300 DPI 的文件体积也会明显增加。格式方面,PNG 无损,适合文字、图表、印章;JPG 体积小,但原 PDF 有透明背景时,转 JPG 可能把透明区域填成黑色。默认用 PNG 更稳妥。

如果面对的是一个目录里的多个 PDF,可以在外层再套一层遍历。rglob('*.pdf') 会递归查找子目录,适合文档库归档;只处理顶层文件时改成 glob('*.pdf')。输出目录可以按原相对路径组织,避免同名文件互相覆盖。
  1. import fitz
  2. from pathlib import Path
  3. def batch_convert(input_dir, output_root=None, dpi=200, img_format='png'):
  4.     input_dir = Path(input_dir)
  5.     pdf_files = list(input_dir.rglob('*.pdf'))
  6.     print(f'找到 {len(pdf_files)} 个PDF文件')
  7.     for pdf_path in pdf_files:
  8.         if output_root:
  9.             relative_path = pdf_path.parent.relative_to(input_dir)
  10.             out_dir = Path(output_root) / relative_path
  11.         else:
  12.             out_dir = pdf_path.parent / f'images_{pdf_path.stem}'
  13.         pdf_to_images(pdf_path, output_dir=out_dir, dpi=dpi, img_format=img_format)
复制代码

实际运行中最容易遇到几类问题。中文路径或文件名报编码错误时,可以在脚本顶部重设标准输出编码,并用 Path.resolve() 取得绝对路径后再传给 fitz.open;路径里如果包含特殊符号,也要先确认能否被正确解析。
  1. import sys
  2. import io
  3. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  4. pdf_path = Path(pdf_path).resolve()
复制代码

带密码的 PDF 不能直接打开,需要判断 needs_pass 并调用 authenticate。authenticate 返回布尔值,不能写成和 True 比较的形式。部分 PDF 即使限制了打印或编辑,屏幕渲染仍可能可用,不能仅凭权限提示判断为完全不能转图片。
  1. doc = fitz.open(pdf_path)
  2. if doc.needs_pass:
  3.     if doc.authenticate('你的密码'):
  4.         print('密码正确')
  5.     else:
  6.         raise ValueError('无法解锁PDF')
复制代码

大 PDF 内存上涨时,不要长期保留页面对象。循环中用 doc.load_page(i) 按需加载,每轮渲染完成后对象更容易释放;如果仍然担心内存峰值,可以分段处理,例如先渲染前 200 页,保存并关闭文档,再重新打开处理后续页面。原文实测 1000 页文档连续渲染 200 DPI 的 JPG,内存稳态约 1.5 GB,属于可接受范围。
  1. for i in range(doc.page_count):
  2.     page = doc.load_page(i)
  3.     pix = page.get_pixmap(dpi=200)
  4.     pix.save(f'page_{i+1}.png')
复制代码

输出图片颜色偏色或整体发黑,常见原因是颜色空间转换。可以显式指定 colorspace=fitz.csRGB 让屏幕预览按 RGB 渲染;需要 CMYK 时可指定 fitz.csCMYK,但普通 PNG 默认仍以 RGB 方式存储,缺少 ICC 配置时看图软件显示可能发灰。原文的建议是屏幕预览一律 RGB,印刷需求交给印前流程处理,不依赖 Python 管理色彩。扫描版 PDF 转图片时,页面本身就是一张大图,重点应放在分辨率上,后续要提取文字应走 OCR,而不是期待转图片解决乱码。
  1. pix = page.get_pixmap(dpi=300, colorspace=fitz.csRGB)
复制代码

进一步优化时,可以只抽取关键页,用页码范围过滤减少无用输出;黑白文档可以输出灰度图,原文指出灰度文件体积大约是 RGB 的三分之一。生成缩略图时,可把前若干页按网格拼成一张预览图,原文示例最多拼接 16 页,并引入 PIL 处理图片尺寸和画布。
  1. import fitz
  2. from PIL import Image
  3. import io
  4. def make_thumbnail(pdf_path, out_png, cols=4, thumb_width=200, dpi=60):
  5.     doc = fitz.open(pdf_path)
  6.     thumbs = []
  7.     max_pages = min(16, doc.page_count)
  8.     for i in range(max_pages):
  9.         pix = doc[i].get_pixmap(dpi=dpi)
  10.         img = Image.open(io.BytesIO(pix.tobytes('png')))
  11.         ratio = thumb_width / img.width
  12.         new_h = int(img.height * ratio)
  13.         img = img.resize((thumb_width, new_h))
  14.         thumbs.append(img)
  15.     doc.close()
  16.     rows = (len(thumbs) + cols - 1) // cols
  17.     max_h = max(img.height for img in thumbs)
  18.     canvas = Image.new('RGB', (cols * thumb_width, rows * max_h), 'white')
  19.     for i, img in enumerate(thumbs):
  20.         x = (i % cols) * thumb_width
  21.         y = (i // cols) * max_h
  22.         canvas.paste(img, (x, y))
  23.     canvas.save(out_png)
复制代码

长任务建议加进度条。安装 tqdm 后,把页码循环改成带描述的迭代,处理 500 页 PDF 时能直观看到进度,避免误以为程序卡死。原文给出的普通笔记本速度约为每秒 3 到 5 页,200 DPI 输出 PNG。
  1. from tqdm import tqdm
  2. for i in tqdm(page_indices, desc='转换进度', unit='页'):
  3.     page = doc[i]
  4.     pix = page.get_pixmap(dpi=dpi)
  5.     pix.save(out_template.format(i + 1))
复制代码

如果经常在资源管理器里操作 PDF,还可以用 reg add 注册右键菜单命令,把选中的文件作为 %1 传给脚本,并用 pythonw.exe 避免弹出命令行窗口。注册表路径和脚本路径都应使用绝对路径,生产环境先在测试机验证。

整体来看,这套脚本的核心不是替代某一次手工导出,而是把 PDF 转图片变成可重复调用的自动化节点:不依赖在线服务,不依赖 Adobe 系软件,参数、命名和异常处理都由代码控制。首次使用时尤其要确认安装的是 PyMuPDF、导入的是 fitz;批处理前检查 doc.page_count 是否大于 0;损坏文件用 try/except 包裹并输出文件路径和异常类型,排查效率会高很多。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: PyMuPDF批量PDF转图片脚本:DPI参数、页码过滤与内存排查

这个脚本挺实用,尤其是把 DPI、页码和输出格式做成参数,批量离线处理很合适。我读下来有两点想补充:一是函数里 pages 目前按 0 基索引过滤,如果命令行 --pages 想按用户习惯从 1 开始,记得解析时统一减 1,否则容易差一页;二是高 DPI 时内存要留意,300 DPI 的 A4 单页位图就不小,批量时最好逐页保存并及时释放 pix,或者用 alpha=False 降低占用。另外建议把 doc.close 放进 finally,或者用 with fitz.open 管理文档,遇到损坏页、加密 PDF 时输出更可控。整体思路清晰,zfill 命名也方便后续排序。
回复 支持 反对

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: PyMuPDF批量PDF转图片脚本:DPI参数、页码过滤与内存排查

这版脚本思路挺完整,参数化、pathlib 路径处理和 zfill 命名都很实用,适合内网离线批量转图。实际跑大批量时我会重点关注 DPI 和内存:300 DPI 下单页位图占用的内存不小,页数多时最好逐页渲染、保存后及时释放 pix,不要把所有图片对象都留在列表里;可以先用低 DPI 试跑,确认页码和命名无误后再提高分辨率。页码过滤这里内部是 0 基索引,如果命令行 --pages 按自然页输入,记得统一减 1,避免整体偏一页。格式处理里把 jpg 转成 jpeg 的细节不错,另外建议留意 jpeg 保存时透明通道和颜色模式的处理。整体来说,这个脚本结构清晰,异常提示和进度输出也够用,稍作参数校验就能直接用于批量任务。
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: PyMuPDF批量PDF转图片脚本:DPI参数、页码过滤与内存排查

这个脚本整体思路很清晰,把输出目录、DPI、格式、页码都抽成参数,批量跑起来会舒服很多,zfill(4) 补零保证按页排序这个细节也挺实用。实际用的时候有几个地方值得留意: 一是页码基准。命令行里 --pages 1 3 5 是给人看的从 1 开始,但函数内部 pages 是按 0 开始的索引来过滤的,入口处最好统一减 1,不然用户以为转的是第 1、3、5 页,出来其实是第 2、4、6 页。另外过滤后建议 sorted 一下,保证输出顺序和原页序一致。 二是句柄和异常。批量处理时中途某个 PDF 损坏或某页渲染报错,doc.close() 可能被跳过,文件被占着后面就删不掉或改不了。用 with fitz.open(...) 或 try/finally 包一层更稳。 三是参数解析。手写扫 sys.argv 遇到缺值、参数顺序打乱、多值参数时容易越界,换成 argparse 会让鲁棒性和用法提示都好很多。 四是内存。高 DPI 下每页 pix 对象都不小,转完一页 save 之后及时 del pix,别在一轮循环里把位图都攒着;大批量的话可以按页区间分几批处理,出问题也容易定位到具体页。DPI 和 matrix 建议只传一个,两个都传容易互相覆盖,排查起来反而绕。 最后提一句,首帖代码在 --pages 那段的解析好像没贴完,后半个判断和调用部分缺失了,方便的话补上完整版会更便于照着
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-20 07:56 , Processed in 0.024223 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部