提取PDF里的图片看起来只是“遍历页面、把图片存下来”,真正动手时会遇到三类麻烦:图片名带斜杠无法直接当文件名、同一张图在多页被引用导致重复保存、以及“要原始素材”还是“要带标注的视觉快照”分不清。下面按 pypdf、PyMuPDF、pdfplumber 三种实现整理一套可落地的脚本,并给出参数含义与常见坑的排查思路。
一、环境准备与方案定位
原文用 conda 建独立环境,Python 版本固定为 3.11,安装 pypdf:
- conda create -n pdf2img python=3.11 -y
- conda activate pdf2img
- conda install -y pypdf
复制代码
三个库的定位差别很明确:pypdf 是纯 Python 实现,安装即用、无系统级 C 依赖,适合图片格式标准、不需要裁剪的场景;PyMuPDF(导入名为 fitz)功能最完整,能按交叉引用表直接取出嵌入图片的原始字节,保留 JPEG/PNG/TIFF 等原始格式并处理 CMYK 到 RGB 的转换;pdfplumber 不能直接取原始嵌入图片,但能拿到图片在页面上的精确边界框,适合按位置裁剪渲染。
二、pypdf 方案:最轻量的纯 Python 实现
核心接口是 page.images,它直接返回图片文件对象,对象上有 name 和 data 两个关键属性。原文实现里做了三处工程化处理,值得单独说明:
1)文件名清洗。img.name 可能是 /I1.png 或 FormXob.xxx.jpg 这类带斜杠的形式,直接拼接路径会出错,所以先 replace("\\", "/") 再取 os.path.basename。
2)扩展名兜底。用 os.path.splitext 拆出主干与后缀,后缀为空时统一补 .png,避免生成无扩展名文件。
3)跨页去重。用 hash(img.data) 作为键维护一个 seen 集合,同一张图被多页引用时只落盘一次。
完整代码如下:
- import os
- import pypdf
- def extract_images_from_pdf(pdf_path, output_dir="extracted_images"):
- os.makedirs(output_dir, exist_ok=True)
- try:
- pdf_reader = pypdf.PdfReader(pdf_path)
- except FileNotFoundError:
- print(f"错误: 找不到PDF文件 '{pdf_path}'")
- return
- except Exception as e:
- print(f"打开PDF失败: {e}")
- return
- print(f"PDF文件: {pdf_path}")
- print(f"总页数: {len(pdf_reader.pages)}")
- image_count = 0
- seen = set()
- for page_num, page in enumerate(pdf_reader.pages):
- print(f"\n处理第 {page_num + 1} 页...")
- try:
- for img in page.images:
- safe_name = os.path.basename(img.name.replace("\", "/"))
- stem, ext = os.path.splitext(safe_name)
- if not ext:
- ext = ".png"
- filename = f"page_{page_num + 1}_{stem}{ext}"
- filepath = os.path.join(output_dir, filename)
- key = hash(img.data)
- if key in seen:
- print(f" 跳过重复图片: {filename}")
- continue
- seen.add(key)
- try:
- with open(filepath, "wb") as f:
- f.write(img.data)
- except Exception as e:
- print(f" 保存失败: {e}")
- continue
- image_count += 1
- print(f" 发现图片 {image_count}: {filename} ({len(img.data)} 字节)")
- except Exception as e:
- print(f" 处理第 {page_num + 1} 页时出错: {e}")
- continue
- print(f"\n提取完成!共保存 {image_count} 张图片")
- print(f"图片保存在: {os.path.abspath(output_dir)}")
- def main():
- pdf_path = input("请输入PDF文件路径: ").strip().strip('"').strip("'")
- if not os.path.exists(pdf_path):
- print(f"错误: 文件 '{pdf_path}' 不存在")
- return
- if not pdf_path.lower().endswith(".pdf"):
- print("警告: 文件可能不是PDF格式")
- output_dir = input("请输入输出目录 (默认为 'extracted_images'): ").strip()
- if not output_dir:
- output_dir = "extracted_images"
- extract_images_from_pdf(pdf_path, output_dir)
- if __name__ == "__main__":
- main()
复制代码
运行方式为 python pdf_extract_fixed.py。需要注意,输入路径时额外调用 strip('"') 与 strip("'") 是为了兼容从资源管理器“复制为路径”带引号粘贴的情况,不处理会直接命中“文件不存在”。
该方案的局限在兼容性:pypdf 对部分特殊编码或 CMYK 图片的处理不如 PyMuPDF 稳定,提取结果可能在格式或色彩上出现偏差。
三、PyMuPDF 方案:直接取原始字节,画质无损
思路是先 page.get_images(full=True) 拿到页面上的图片清单,其中 img[0] 是 xref,即图片在 PDF 交叉引用表中的 ID;再把 xref 交给 doc.extract_image(xref),返回字典里 image 是原始字节、ext 是原始扩展名。因为不重新编码,所以画质不损失,命名按“页号 + 图序 + 原始扩展名”组织:
- import fitz # PyMuPDF
- import os
- def extract_images_pymupdf(pdf_path, output_dir="extracted_images"):
- os.makedirs(output_dir, exist_ok=True)
- doc = fitz.open(pdf_path)
- saved = []
- for page_num in range(len(doc)):
- page = doc.load_page(page_num)
- images = page.get_images(full=True)
- for img_index, img in enumerate(images):
- xref = img[0]
- base_image = doc.extract_image(xref)
- image_bytes = base_image["image"]
- image_ext = base_image["ext"]
- filename = f"page_{page_num + 1}_img_{img_index + 1}.{image_ext}"
- filepath = os.path.join(output_dir, filename)
- with open(filepath, "wb") as f:
- f.write(image_bytes)
- saved.append(filepath)
- print(f"已保存: {filename} ({len(image_bytes) / 1024:.1f} KB)")
- doc.close()
- return saved
- if __name__ == "__main__":
- extract_images_pymupdf("your_file.pdf", "output_images")
复制代码
若同一张图被多页引用,get_images(full=True) 会在每一页都返回同一个 xref,此时用 xref 集合去重比按字节比较更省内存:
- seen_xrefs = set()
- for page in doc:
- for img in page.get_images(full=True):
- xref = img[0]
- if xref in seen_xrefs:
- continue
- seen_xrefs.add(xref)
- # 此处调用 doc.extract_image(xref) 提取
复制代码
四、进阶:需要保留叠加标注时改为区域渲染
如果图片上方叠加了红框、文字说明等非嵌入元素,直接 extract_image 只会拿到原始图片,叠加内容全部丢失。这时应先用 page.get_image_rects(xref) 取图片在页面上的矩形区域,再用 get_pixmap(clip=rect, dpi=300) 渲染该区域。dpi 控制输出分辨率,clip 保证只渲染图片所在范围而不是整页:
- def extract_images_with_annotations(pdf_path, output_dir="extracted_with_annotations"):
- os.makedirs(output_dir, exist_ok=True)
- doc = fitz.open(pdf_path)
- for page_num in range(len(doc)):
- page = doc.load_page(page_num)
- for img_index, img in enumerate(page.get_images(full=True)):
- xref = img[0]
- rects = page.get_image_rects(xref)
- for rect in rects:
- pix = page.get_pixmap(clip=rect, dpi=300)
- filename = f"page_{page_num + 1}_img_{img_index + 1}_annotated.png"
- pix.save(os.path.join(output_dir, filename))
- doc.close()
复制代码
五、pdfplumber 方案:按页面坐标裁剪渲染
pdfplumber 的 page.images 提供每张图的边界信息,但它的坐标原点在左下角,而渲染用的 Pillow 以左上角为原点,所以必须做一次纵向翻转:新的上边界为 page_height - img["y1"],新的下边界为 page_height - img["y0"],然后 page.crop(bbox).to_image(resolution=300) 得到快照并保存为 PNG。
- import pdfplumber
- import os
- def extract_images_pdfplumber(pdf_path, output_dir="pdfplumber_images", resolution=300):
- os.makedirs(output_dir, exist_ok=True)
- saved = []
- with pdfplumber.open(pdf_path) as pdf:
- for page_num, page in enumerate(pdf.pages):
- page_height = page.height
- for img_index, img in enumerate(page.images):
- bbox = (
- img["x0"],
- page_height - img["y1"],
- img["x1"],
- page_height - img["y0"],
- )
- cropped = page.crop(bbox)
- image_obj = cropped.to_image(resolution=resolution)
- filename = f"page_{page_num + 1}_img_{img_index + 1}.png"
- filepath = os.path.join(output_dir, filename)
- image_obj.save(filepath)
- saved.append(filepath)
- print(f"已保存: {filename}")
- return saved
复制代码
代价要提前说清:这种方式得到的是页面区域的像素快照,不是原始嵌入图片。输出格式被统一成 PNG,文件体积通常比原始图片大,清晰度由 resolution 参数决定,与原始图片固有分辨率无关。
六、四个容易踩的坑
第一,重复提取。同一 xref 在多页出现时会被重复返回,PyMuPDF 用 xref 集合去重,pypdf 用图片字节哈希去重,思路一致但成本不同。
第二,“提取原始图片”与“截取页面区域”是两件事。嵌入对象用 extract_image(xref) 得到无损原图,但会丢标注;扫描件或叠加过标注的截图属于页面渲染结果的一部分,只能用 get_pixmap(clip=bbox) 或 page.crop() 渲染区域。选哪个取决于要“原始素材”还是“视觉快照”。
第三,加密 PDF。有打开密码的文件需要在 fitz.open() 后调用 doc.authenticate(password),或打开时直接传 password 参数;仅限制编辑、打印的加密 PDF 通常仍可直接提取。
第四,大文件的内存占用。不要一次性把所有 base_image["image"] 都留在内存里,按页处理、处理完即释放,是官方建议的稳妥做法。
综合来看:追求画质和无损原始格式就选 PyMuPDF;部署环境不能装 C 依赖、图片格式规范就选 pypdf;需要按版面精确截图则用 pdfplumber。 |