PDF转图片看似简单,真正批量处理时常见问题不少:在线工具限制文件大小,阅读器导出分辨率不可控,内网环境又不适合上传文档。把转换逻辑写成 Python 脚本,调用 MuPDF 渲染引擎逐页输出位图,更适合批量、离线、可配置的场景。原文围绕 PyMuPDF 展开,重点在 DPI 控制、页码过滤、输出命名和异常排查。
在方案选择上,pdf2image 依赖 poppler,Windows 下要单独配置 PATH;pdftoppm 是系统命令行工具,做简单转换方便,但要做目录遍历、命名规则和异常处理时脚本会变复杂。PyMuPDF 通过 pip 安装,导入名是 fitz,不依赖额外系统动态库,跨平台安装相对省事。需要注意:应安装 PyMuPDF,而不是安装名为 fitz 的老库;安装完成后用 import fitz 验证导入是否成功。
环境准备建议使用独立虚拟环境,避免污染全局 Python:- python -m venv pdf2img_env
- # Windows PowerShell 激活 Scripts 目录下的 Activate.ps1
- # Linux/macOS 激活:
- source pdf2img_env/bin/activate
- pip install --upgrade pip
- 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。- import fitz
- doc = fitz.open('原始文件.pdf')
- page = doc[0]
- # 直接指定 DPI
- pix = page.get_pixmap(dpi=300)
- pix.save('第1页.png')
- # 指定缩放矩阵,2.0 倍约等于 144 DPI
- zoom = fitz.Matrix(2.0, 2.0)
- pix = page.get_pixmap(matrix=zoom)
复制代码
下面这版脚本把输出目录、DPI、图片格式和页码范围都做成参数。页码对外从 1 开始,内部索引从 0 开始;输出文件名用 zfill(4) 补零,便于按页排序。pathlib.Path 负责路径拼接,可以减少 Windows 反斜杠转义问题。- import fitz
- import sys
- from pathlib import Path
- def pdf_to_images(pdf_path, output_dir=None, dpi=200, img_format='png', pages=None):
- pdf_path = Path(pdf_path)
- if not pdf_path.exists():
- raise FileNotFoundError(f'找不到PDF文件: {pdf_path}')
- if output_dir is None:
- output_dir = pdf_path.parent / f'images_{pdf_path.stem}'
- output_dir = Path(output_dir)
- output_dir.mkdir(parents=True, exist_ok=True)
- doc = fitz.open(pdf_path)
- total = doc.page_count
- if pages is None:
- page_indices = range(total)
- else:
- page_indices = [p for p in pages if 0 <= p < total]
- if not page_indices:
- print('警告:指定的页码范围无效')
- return
- print(f'PDF页数: {total},本次转换: {len(page_indices)} 页')
- for i, page_idx in enumerate(page_indices, start=1):
- page = doc[page_idx]
- pix = page.get_pixmap(dpi=dpi)
- suffix = str(page_idx + 1).zfill(4)
- ext = img_format.lower().replace('jpg', 'jpeg')
- out_file = output_dir / f'{pdf_path.stem}_{suffix}.{ext}'
- pix.save(out_file)
- print(f'[{i}/{len(page_indices)}] 已生成: {out_file.name}')
- doc.close()
- print(f'处理完成,共生成 {len(page_indices)} 张图片,保存在:{output_dir}')
- return output_dir
- if __name__ == '__main__':
- if len(sys.argv) < 2:
- print('用法: python pdf2img.py <pdf文件> [--dpi 300] [--format png] [--pages 1 3 5]')
- sys.exit(1)
- pdf_arg = sys.argv[1]
- kwargs = {}
- if '--dpi' in sys.argv:
- idx = sys.argv.index('--dpi')
- kwargs['dpi'] = int(sys.argv[idx + 1])
- if '--format' in sys.argv:
- idx = sys.argv.index('--format')
- kwargs['img_format'] = sys.argv[idx + 1]
- if '--pages' in sys.argv:
- idx = sys.argv.index('--pages')
- pages = []
- for v in sys.argv[idx + 1:]:
- if v.isdigit():
- pages.append(int(v) - 1)
- else:
- break
- kwargs['pages'] = pages
- try:
- pdf_to_images(pdf_arg, **kwargs)
- except Exception as e:
- print(f'转换失败: {e}')
- 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')。输出目录可以按原相对路径组织,避免同名文件互相覆盖。- import fitz
- from pathlib import Path
- def batch_convert(input_dir, output_root=None, dpi=200, img_format='png'):
- input_dir = Path(input_dir)
- pdf_files = list(input_dir.rglob('*.pdf'))
- print(f'找到 {len(pdf_files)} 个PDF文件')
- for pdf_path in pdf_files:
- if output_root:
- relative_path = pdf_path.parent.relative_to(input_dir)
- out_dir = Path(output_root) / relative_path
- else:
- out_dir = pdf_path.parent / f'images_{pdf_path.stem}'
- pdf_to_images(pdf_path, output_dir=out_dir, dpi=dpi, img_format=img_format)
复制代码
实际运行中最容易遇到几类问题。中文路径或文件名报编码错误时,可以在脚本顶部重设标准输出编码,并用 Path.resolve() 取得绝对路径后再传给 fitz.open;路径里如果包含特殊符号,也要先确认能否被正确解析。- import sys
- import io
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
- pdf_path = Path(pdf_path).resolve()
复制代码
带密码的 PDF 不能直接打开,需要判断 needs_pass 并调用 authenticate。authenticate 返回布尔值,不能写成和 True 比较的形式。部分 PDF 即使限制了打印或编辑,屏幕渲染仍可能可用,不能仅凭权限提示判断为完全不能转图片。- doc = fitz.open(pdf_path)
- if doc.needs_pass:
- if doc.authenticate('你的密码'):
- print('密码正确')
- else:
- raise ValueError('无法解锁PDF')
复制代码
大 PDF 内存上涨时,不要长期保留页面对象。循环中用 doc.load_page(i) 按需加载,每轮渲染完成后对象更容易释放;如果仍然担心内存峰值,可以分段处理,例如先渲染前 200 页,保存并关闭文档,再重新打开处理后续页面。原文实测 1000 页文档连续渲染 200 DPI 的 JPG,内存稳态约 1.5 GB,属于可接受范围。- for i in range(doc.page_count):
- page = doc.load_page(i)
- pix = page.get_pixmap(dpi=200)
- pix.save(f'page_{i+1}.png')
复制代码
输出图片颜色偏色或整体发黑,常见原因是颜色空间转换。可以显式指定 colorspace=fitz.csRGB 让屏幕预览按 RGB 渲染;需要 CMYK 时可指定 fitz.csCMYK,但普通 PNG 默认仍以 RGB 方式存储,缺少 ICC 配置时看图软件显示可能发灰。原文的建议是屏幕预览一律 RGB,印刷需求交给印前流程处理,不依赖 Python 管理色彩。扫描版 PDF 转图片时,页面本身就是一张大图,重点应放在分辨率上,后续要提取文字应走 OCR,而不是期待转图片解决乱码。- pix = page.get_pixmap(dpi=300, colorspace=fitz.csRGB)
复制代码
进一步优化时,可以只抽取关键页,用页码范围过滤减少无用输出;黑白文档可以输出灰度图,原文指出灰度文件体积大约是 RGB 的三分之一。生成缩略图时,可把前若干页按网格拼成一张预览图,原文示例最多拼接 16 页,并引入 PIL 处理图片尺寸和画布。- import fitz
- from PIL import Image
- import io
- def make_thumbnail(pdf_path, out_png, cols=4, thumb_width=200, dpi=60):
- doc = fitz.open(pdf_path)
- thumbs = []
- max_pages = min(16, doc.page_count)
- for i in range(max_pages):
- pix = doc[i].get_pixmap(dpi=dpi)
- img = Image.open(io.BytesIO(pix.tobytes('png')))
- ratio = thumb_width / img.width
- new_h = int(img.height * ratio)
- img = img.resize((thumb_width, new_h))
- thumbs.append(img)
- doc.close()
- rows = (len(thumbs) + cols - 1) // cols
- max_h = max(img.height for img in thumbs)
- canvas = Image.new('RGB', (cols * thumb_width, rows * max_h), 'white')
- for i, img in enumerate(thumbs):
- x = (i % cols) * thumb_width
- y = (i // cols) * max_h
- canvas.paste(img, (x, y))
- canvas.save(out_png)
复制代码
长任务建议加进度条。安装 tqdm 后,把页码循环改成带描述的迭代,处理 500 页 PDF 时能直观看到进度,避免误以为程序卡死。原文给出的普通笔记本速度约为每秒 3 到 5 页,200 DPI 输出 PNG。- from tqdm import tqdm
- for i in tqdm(page_indices, desc='转换进度', unit='页'):
- page = doc[i]
- pix = page.get_pixmap(dpi=dpi)
- pix.save(out_template.format(i + 1))
复制代码
如果经常在资源管理器里操作 PDF,还可以用 reg add 注册右键菜单命令,把选中的文件作为 %1 传给脚本,并用 pythonw.exe 避免弹出命令行窗口。注册表路径和脚本路径都应使用绝对路径,生产环境先在测试机验证。
整体来看,这套脚本的核心不是替代某一次手工导出,而是把 PDF 转图片变成可重复调用的自动化节点:不依赖在线服务,不依赖 Adobe 系软件,参数、命名和异常处理都由代码控制。首次使用时尤其要确认安装的是 PyMuPDF、导入的是 fitz;批处理前检查 doc.page_count 是否大于 0;损坏文件用 try/except 包裹并输出文件路径和异常类型,排查效率会高很多。 |