资讯详情 Python批量将JPG/PNG转PDF:多图合并与命令行实践
📅 2026/10/12 6:43:28
把 JPG / PNG 转成 PDF这个需求看起来小到不能再小真做起来却到处都是坑。在线转换网站倒是抓一把有一把但要么限制文件数量要么偷偷压缩画质要么传完图片还要盯着广告等进度条遇到几十张合同扫描件要合成一个 PDF 发出去这种方案基本没法用。前阵子我帮朋友处理一批扫描发票和课程截图顺手用 Python 把整套流程彻底捋了一遍单张转、批量转、多图合并成一个 PDF再带命令行入口全程本地处理、不传文件、不限张数核心代码写出来真没多少。这篇文章就把这套方案完整拆给你环境怎么搭、原理是什么、代码怎么写以及我踩过的几个比较隐蔽的坑。1. 动手之前先把需求掰开揉碎1.1 这个需求到底长什么样图片转 PDF 看起来是同一个动作但背后对应着完全不同的使用场景。最典型的是合同扫描归档手机拍了一堆 JPG要按顺序合成一个多页 PDF 发给客户或法务。其次是课件和资料整理截图、思维导图、拍照笔记想把它们统一成 PDF 方便在手机和电脑上翻看。还有一类是报销和存档发票、报销单、快递单都是图片格式财务系统往往只收 PDF这时候批量转格式就成了刚需。有意思的是我看到有人还在搜“jpg 文件怎么改成 zip”其实这类需求多半是想把一堆图片打包或者统一处理格式。真到了归档和传阅那一步把图片转成 PDF 往往比压缩成 zip 更直接——PDF 不需要对方额外安装看图软件打印、签章、标注都方便。所以这篇文章不只是在讲“怎么转”而是把“单张转换、批量转换、多图合并、尺寸统一、透明背景处理”这一整套问题一起解决掉。1.2 三条技术路线怎么选Python 做这件事有三条主流路线我先把结论放在前面日常处理用 Pillow无损归档用 img2pdf复杂排版用 reportlab。方案核心库优点缺点适合场景PillowPillow能统一处理 JPG/PNG 差异可控画质、尺寸、方向代码简单重新编码图像理论上会有一点画质损失日常转换、合并、加白边、改页面大小img2pdfimg2pdf图像原始字节直接嵌入 PDF画质零损失文件小对带透明通道的 PNG 不友好混合尺寸需要额外配置扫描件、合同、照片无损归档reportlabreportlab能精确控制版面、添加文字水印、绘制表格代码量大把图片塞进 PDF 反而绕远路要做排版复杂的 PDF 报告Pillow 之所以是主力是因为它的读写能力覆盖了绝大多数图片格式JPG 的压缩、PNG 的透明通道、图像旋转缩放它都管。而“合并”这个需求Pillow 的 save 方法原生支持把一个图片列表写进同一个 PDF每一张图对应一页不需要自己拼版面。img2pdf 更适合追求绝对原画质的场景但如果图片是带透明的 PNG它不会帮你处理背景打印出来容易黑一块。考虑到大多数人的核心诉求是“能用、方便、效果正常”这篇文章以 Pillow 为主线img2pdf 作为补充方案单独给你一段代码。2. 环境准备从零到能跑别卡在第一步2.1 Python 装好了吗如果你电脑上还没有 Python先去官网下载安装包。Windows 用户特别注意一件事安装第一步那个勾选框 “Add python.exe to PATH” 一定要勾上不勾的话后面在终端敲 python 会提示“不是内部或外部命令”这种问题我见过太多次了。装完以后打开终端Windows 按 WinR 输入 cmdmacOS 用 Spotlight 搜 Terminal输入python --version如果能显示 Python 3.x 的版本号说明环境没问题。如果提示找不到命令先看看是不是 PATH 没加或者重启一下终端再试。macOS 和 Linux 一般自带 Python 3但版本可能偏老建议也装一个新版避免后面 pip 装依赖时碰到兼容性问题。2.2 把依赖装齐接下来安装两个库pip install pillow img2pdf国内网络下载慢的话加清华源pip install pillow img2pdf -i https://pypi.tuna.tsinghua.edu.cn/simple装完后验证一下python -c from PIL import Image; import img2pdf; print(ok)这里有个细节PIL是旧包名Pillow 是它的延续import 的时候还是写from PIL import Image新手别搞混。img2pdf 我们后面只用一小段它是纯 Python 实现安装时不会给你惹什么大麻烦。2.3 VSCode 里踩过的两个小坑如果你习惯用 VSCode 写 Python建议装上官方 Python 扩展然后按 CtrlShiftP输入 “Python: Select Interpreter”选择刚才装好的那个解释器。这一步不做的话代码可能能跑但 IDE 会满屏波浪线调试也调不顺。另外一个容易忽略的地方是终端激活环境。有的人用 conda 或者 pyenv 管理 Python 版本VSCode 默认终端和你命令行用的解释器可能不是同一个。最简单的判断方法是在 VSCode 终端里运行python -c import PIL; print(PIL.__file__)打印出来的路径如果指向你刚装 Pillow 的那个环境就放心写代码了。2.4 先准备一份测试素材写代码之前建议你在本地建一个文件夹比如images_to_pdf丢几张 JPG 和 PNG 进去当测试素材。文件命名最好带数字序号比如01.jpg、02.png、03.jpg这样能顺便验证代码处理文件排序是否正确。我后面会专门讲文件排序的坑很多人在这一步吃过亏。3. 核心原理图片是怎么变成 PDF 页面的3.1 PDF 在底层其实是一堆页面对象很多人以为 PDF 是一种像 Word 那样的文档格式其实 PDF 更像一个“打印包”它里面记录了页面的大小、每页包含哪些元素、每个元素用什么颜色空间来描述。图片嵌入 PDF 时会被转换成一个图像对象然后放在某个页面上。页面大小通常以“点”为单位1 点等于 1/72 英寸。这个概念听起来抽象但对理解转换行为很重要。比如你拿一张 1920×1080 的图片不指定任何参数直接转 PDF生成的 PDF 页面大小就是 1920×1080 点大概 26.7×15 英寸。这种 PDF 在屏幕上看得挺大打印出来却会裁切或者留白。所以做“合并”的时候页面尺寸统一是绕不开的问题。3.2 Pillow 是怎么把图片写进 PDF 的Pillow 的Image.save()方法遇到.pdf后缀时会走 PDF 编码逻辑。单张图片的时候直接img.save(output.pdf)就能得到一个单页 PDF。多张图片合并时需要两个参数save_allTrue告诉 Pillow 这是一个多页文档append_images[...]提供后续每一页的图像对象。它的工作流程是把每张图片做必要的模式转换写进同一个 PDF 文件流然后按顺序生成页面目录。所以你在代码里看到的“合并”本质上不是把图片拼在一张纸上而是把多张图片按顺序排列成 PDF 的多页。3.3 尺寸、DPI、页面大小怎么换算这部分是很多人忽略、但实际最影响结果的地方。PDF 允许你通过resolution参数指定“每英寸多少个像素点”这一概念单位是 DPI。换算关系是像素宽度 ÷ DPI × 2.54 厘米举个例子一张 794 像素宽的图片按 96 DPI 转换物理宽度是 794 ÷ 96 × 2.54 ≈ 21 厘米恰好接近 A4 纸宽度。如果你不设分辨率Pillow 默认可能使用一个比较任意的值结果导致图片在 PDF 里显示得过大或过小。我一般在合并时先取第一张图的尺寸后面每张图如果尺寸不一致就等比缩放到一个公共尺寸内再放到白底画布上。这样整个 PDF 的页面尺寸统一翻页时视觉上不会忽大忽小。3.4 透明 PNG 和颜色模式JPG 没有透明通道但 PNG 有。Pillow 读取 PNG 时如果它带透明信息图像模式通常会是RGBA或者LA。直接把这些图像保存成 PDF 会遇到两个问题一是某些模式 Pillow 不支持直接写入 PDF会报错二是就算能写透明区域在 PDF 里往往会被渲染成黑色因为 PDF 的显示引擎不保证支持 alpha 通道。解决方法是把所有图片统一转成RGB模式img.convert(RGB)。这行代码会把透明通道丢弃并用当前背景色替换透明区域。默认背景色是黑色所以把白底需求做进去就是先创建一张白色背景图再把 PNG 贴上去最后转 RGB。我在代码里给你写好了这个逻辑。4. 代码实现单张、批量、合并、命令行一把梭4.1 单张图片转 PDF先跑通最小闭环先从最简单的开始熟悉 Pillow 的基本写法from PIL import Image # 打开图片转成 RGB 模式 img Image.open(01.jpg).convert(RGB) # 保存为 PDFresolution 控制清晰度 img.save(output.pdf, PDF, resolution150.0)resolution150.0是我比较推荐的值。打印需求一般 150 DPI 足够A4 纸打印 300 DPI 虽然更细腻但文件体积会明显变大。如果只是屏幕查阅和微信传输150 就已经很清晰了。这一步跑通后你已经完成了“把 JPG / PNG 转成 PDF”的最小闭环。4.2 多张图片合并成一个多页 PDF接下来是重头戏把文件夹里的所有 JPG、PNG 按顺序合成一个 PDF。直接上完整代码import os from PIL import Image def images_to_pdf(image_paths, output_pdf): img_list [] for path in image_paths: img Image.open(path).convert(RGB) img_list.append(img) # 第一张图作为首页其余图片作为附加页 img_list[0].save( output_pdf, PDF, save_allTrue, append_imagesimg_list[1:], resolution150.0 ) # 示例读取当前目录下所有图片 folder images paths [ os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith((.jpg, .jpeg, .png)) ] paths.sort() # 按文件名排序 images_to_pdf(paths, merged.pdf) print(合并完成共, len(paths), 页)这里有几个关键点。第一paths.sort()直接按字符串排序文件名如果是1.jpg、10.jpg、2.jpg排序结果会是1.jpg、10.jpg、2.jpg顺序是错的。正确做法是使用自然排序import re def natural_key(filename): return [int(t) if t.isdigit() else t for t in re.split(r(\d), filename)] paths.sort(keynatural_key)第二代码里把图片全部读进内存再保存如果图片特别多比如上百张手机原图内存会吃紧。优化思路是边读边写但 Pillow 的save_all会先持有全部图片对象。我建议超过 50 张图时分批次处理或者用后面 img2pdf 的方案它的流式处理对内存友好得多。4.3 统一页面尺寸避免大小不一如果图片尺寸差异很大直接合并会让 PDF 页面大小各不相同翻页体验非常割裂。我的做法是以第一张图为基准或者指定一个目标宽度把其他图片等比缩放后放到白色画布上。from PIL import Image def normalize_image(img, target_width1600, bg_color(255, 255, 255)): # 等比缩放到目标宽度 ratio target_width / img.width new_height int(img.height * ratio) img img.resize((target_width, new_height), Image.LANCZOS) # 在白色画布上居中放置 canvas Image.new(RGB, (target_width, new_height), bg_color) canvas.paste(img, (0, 0)) return canvas这段代码同时解决了两个问题统一了页面宽度消除了透明通道。Image.LANCZOS是高品质重采样算法缩放带来的画质损失肉眼基本看不出区别。目标宽度我习惯取 1600对应 96 DPI 下大约是 42 厘米宽屏幕查看绰绰有余如果要打印可以用 2100 左右。4.4 做一个带命令行参数的小工具光有函数还不够实际用的时候你会发现每次都要改路径太麻烦。我顺手封装了一个很简短的命令行脚本import argparse import os import re from PIL import Image def natural_key(filename): return [int(t) if t.isdigit() else t for t in re.split(r(\d), filename)] def main(): parser argparse.ArgumentParser(description将 JPG/PNG 转换为 PDF) parser.add_argument(input_dir, help图片文件夹路径) parser.add_argument(-o, --output, defaultmerged.pdf, help输出 PDF 文件名) parser.add_argument(-w, --width, typeint, default1600, help统一宽度默认 1600) args parser.parse_args() folder args.input_dir paths [ os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith((.jpg, .jpeg, .png)) ] if not paths: print(文件夹里没有图片) return paths.sort(keynatural_key) img_list [] for path in paths: img Image.open(path).convert(RGB) ratio args.width / img.width new_h int(img.height * ratio) img img.resize((args.width, new_h), Image.LANCZOS) img_list.append(img) img_list[0].save( args.output, PDF, save_allTrue, append_imagesimg_list[1:], resolution150.0 ) print(f已输出 {args.output}共 {len(img_list)} 页) if __name__ __main__: main()保存成img2pdf.py注意别和那个第三方库img2pdf搞混文件名最好不要重名用法是python img2pdf.py ./images -o archive.pdf -w 1600有了这个脚本以后在终端里一条命令就完成整个文件夹的合并。Windows 用户还可以把它写成一个.bat文件拖拽文件夹就能执行基本上算是“桌面级”体验了。4.5 零损耗方案img2pdf 无损转档追求原画质无损失的时候用 img2pdf。比如扫描件、证据材料、专业摄影图任何肉眼都不可接受的压缩都不行。import img2pdf import os folder scan paths [ os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith((.jpg, .png)) ] paths.sort() with open(scanned.pdf, wb) as f: f.write(img2pdf.convert(paths))img2pdf 的优点是快、准、稳缺点是它不理解“透明”概念RGBA 的 PNG 直接用它处理容易出问题。所以我的经验是扫描件、照片这类无不透明像素的场景直接闭眼用 img2pdf带透明 PNG、需要加白底、需要统一页面的场景用 Pillow。4.6 图片来自网页的 data URL 怎么办有人会遇到网页上复制下来的图片是一长串以data:image/png;base64开头的东西那不是文件路径而是图片的 base64 编码。直接扔给 Pillow 会报错。先解码保存成临时文件再处理import base64 from PIL import Image from io import BytesIO # 假设 data_url 是从网页拿到的 data:image/png;base64,..... b64_data data_url.split(,, 1)[1] img Image.open(BytesIO(base64.b64decode(b64_data))) img.convert(RGB).save(web_img.pdf, PDF, resolution150.0)这个小技巧不常用但遇到一次能帮你省不少事。5. 常见问题与排查实录5.1 “cannot write mode P as PDF”怎么破Pillow 报这个错是因为图片模式是P8 位调色板模式常见于从某些工具保存的 PNG、GIF。convert(RGB)一行就能解决img Image.open(path).convert(RGB)这个错误几乎每个用 Pillow 转 PDF 的人都会遇到看到之后不用慌先看看是不是没有转模式。5.2 透明 PNG 背景变黑前面说过默认丢弃透明通道后会用黑色填充。我踩过这个坑之后养成了习惯只要遇到 PNG先判断它有没有 alpha 通道有就贴到白色画布上再操作。def to_white_bg(img): if img.mode in (RGBA, LA, P): img img.convert(RGBA) bg Image.new(RGB, img.size, (255, 255, 255)) bg.paste(img, maskimg.split()[-1]) # 用 alpha 通道作蒙版 return bg return img.convert(RGB)这段代码的关键是mask参数它把原图的透明信息作为选区让白色底只在透明区域显示不透明区域的文字、图案保持清晰。5.3 图片太多导致内存爆掉批量处理几百张图片时Pillow 把所有图片对象都挂在一个列表里内存占用可能上 GB。我的应对办法是大于 50 张图时就改用 img2pdf或者把图片先压缩好尺寸再喂给 Pillow。另外用Image.open()时还没有真正加载像素数据只有执行convert()或save()时才会解码所以尽量在最后一步才调用这些方法。5.4 文件排序错乱和中文路径文件名排序的问题前面已经细说了字符串排序导致的1.jpg, 10.jpg, 2.jpg是高频问题直接用自然排序函数。中文路径的话Windows 上通常没问题但如果代码报 UnicodeDecodeError可以在处理路径时用pathlib.Path替代字符串拼接它会帮你处理大部分编码差异。from pathlib import Path paths [p for p in Path(images).glob(*) if p.suffix.lower() in (.jpg, .png)]5.5 常见问题速查表问题原因解决方案cannot write mode P as PDF图片是调色板模式 P先convert(RGB)透明 PNG 变成黑底alpha 通道被丢弃白底粘贴后再转 RGBPDF 页面大小不一致图片尺寸不同用统一宽度并等比缩放文件顺序错乱字符串排序问题用正则自然排序内存不足图片加载过多改用 img2pdf 或先缩小图片提示找不到 python 命令安装时没勾 PATH重新安装并勾选 Add to PATH5.6 两个偷懒的进阶玩法最后分享两个我平时偷懒的小技巧。第一个是给脚本做成右键菜单。Windows 用户可以在注册表里加一个自定义命令指向python D:\tools\img2pdf.py %1这样右键点击文件夹就能直接合并成 PDF不过注册表操作要小心新手看文档操作即可。第二个是写一个定时任务每天自动把指定文件夹里的新图片转成 PDF 归档。用 Windows 任务计划程序或 macOS 的 launchd 定时执行python D:/tools/img2pdf.py D:/scans -o D:/archive/scan_$(date %Y%m%d).pdf配合命令行参数这套脚本就能从“手动工具”升级成“自动化流程”。我自己现在处理图片转 PDF日常主力还是 Pillow毕竟它把“转格式、合并、统一尺寸、处理透明”这些事都包圆了只有碰到扫描合同这类完全不能容忍画质折损的场景才会切到 img2pdf。这套方案你拿去以后建议先把单一图片跑通再把合并逻辑加进去最后再考虑参数封装成命令行工具。千万别一上来就照着大而全的代码抄出了问题反而不好定位。等你用顺手了会发现这个脚本还能扩展出很多玩法批量加水印、自动压缩图片体积、OCR 识别后生成带文字的 PDF……但这些都是后话了先把转 PDF 这一步吃透。