
前言把一堆照片、扫描件、截图整理成一个 PDF是日常里特别高频的小需求。手工操作的痛点很清楚图片尺寸不一、方向不一直接丢进 PDF 里有的横有的竖、有的糊有的清图片一多手动一张张处理更是折磨。写一个小命令行工具能一次性解决它一条命令把指定目录或若干文件批量读取、统一尺寸、居中排好最后合成一个 PDF。这个工具虽小却是「命令行程序」的典型骨架——参数解析、输入输出路径处理、错误容忍、依赖声明四件套一件不少。需要提前说明依赖Python 标准库没有图像处理和 PDF 写入的能力。读图、缩放、居中、写出多页 PDF 都依赖第三方库Pillowpip install Pillow导入名仍是PIL。另一条常见路线是用reportlab之类专门的 PDF 生成库但那同样是第三方库且 API 各不相同——本文只用 Pillow 这条线涉及具体参数之处以 Pillow 官方文档为准。一、先定接口一条命令要做什么先把工具的行为说清楚再写代码。设想接口长这样python img2pdf.py a.jpg b.png c.jpg -o 作品集.pdf --width 1240 --height 1754含义是把三个图片文件按给定的每页宽高处理居中放进统一页面输出为作品集.pdf。设计上有几个决定输入用「若干路径」而不是「一个目录」这样既能传单个文件也能配合 shell 通配符批量传如果还想支持目录可以后续再加一个-d/--dir参数页面尺寸用像素给出默认取 A4 在 150 DPI 下的近似值换算关系直观某一页读失败不终止整个程序而是跳过并打印警告——批量处理里一张坏图毁掉整批结果是最恼人的。二、参数解析用 argparse 定义接口标准库的argparse就是为这种场景准备的它负责把命令行字符串变成结构化参数还会自动生成--help。# 适用于 Python 3.8import argparsedef build_parser():parser argparse.ArgumentParser(progimg2pdf,description把若干图片统一尺寸后合成一个 PDF。,)parser.add_argument(inputs, nargs, help输入图片路径可传多个)parser.add_argument(-o, --output, defaultout.pdf, help输出 PDF 路径)parser.add_argument(--width, typeint, default1240, help每页宽度像素)parser.add_argument(--height, typeint, default1754, help每页高度像素)parser.add_argument(--margin, typeint, default0, help页面四周留白像素)return parser几个要点nargs表示这个位置参数至少接收一个值命令行里写几个都收进一个列表typeint让 argparse 自动做类型转换传了非整数会直接报错并给出用法提示default给出可选项的默认值用户不传就是它-o与--output是同一个参数的短名与长名。parse_args()返回一个命名空间对象inputs是列表其余是标量。不要在业务代码里直接读sys.argv——手写解析既难处理--前缀也做不好类型校验和帮助信息。三、批量读取与处理路径、缩略、居中处理单张图片的逻辑要拆成一个纯函数输入一个路径和一个目标尺寸输出一张「已经排好版」的页面。这样它既好测也好复用。# 适用于 Python 3.8需先 pip install Pillowimport sysfrom pathlib import Pathfrom PIL import Imagedef render_page(path, size, margin0, background(255, 255, 255)):把一张图等比缩放后居中放进一个 size 大小的页面。box_w max(1, size[0] - 2 * margin)box_h max(1, size[1] - 2 * margin)with Image.open(path) as src:page_img src.convert(RGB)page_img.thumbnail((box_w, box_h), Image.LANCZOS)offset_x margin (box_w - page_img.width) // 2offset_y margin (box_h - page_img.height) // 2canvas Image.new(RGB, size, background)canvas.paste(page_img, (offset_x, offset_y))return canvas这里刻意用了thumbnail()而不是resize()thumbnail()会原地缩放并保持长宽比、且只缩不放正符合「完整显示、不裁切、不拉伸」的需求。Image.open()用with包住保证文件句柄及时释放。主流程负责遍历输入、报告进度、收集结果# 适用于 Python 3.8需先 pip install Pillowdef collect_pages(inputs, size, margin0):pages []for raw in inputs:path Path(raw)if not path.is_file():print(f跳过不存在的文件{path}, filesys.stderr)continuetry:pages.append(render_page(path, size, margin))except OSError as exc:# Pillow 打不开的文件格式不支持、文件损坏会抛 OSError 及其子类print(f无法读取 {path}{exc}, filesys.stderr)return pages错误处理的两条原则在这里体现得很清楚能预期的错误就地捕获并给出可读提示继续处理后面的文件真正致命的情况一页都没成功留给主流程判断。四、合成 PDF依赖第三方库多页 PDF 的写入同样由 Pillow 完成把第一页当作主对象其余页作为「附加页」一起交出并指明输出格式为 PDF。下面这段的save_all与append_images是 Pillow 常见的多页保存方式参数含义以 Pillow 官方文档为准。# 适用于 Python 3.8需先 pip install Pillowimport sysfrom pathlib import Pathdef main(argvNone):args build_parser().parse_args(argv)size (args.width, args.height)pages collect_pages(args.inputs, size, args.margin)if not pages:print(没有任何可用图片已退出。, filesys.stderr)return 1out_path Path(args.output)out_path.parent.mkdir(parentsTrue, exist_okTrue) # 输出目录不存在就先建first, rest pages[0], pages[1:]# 传给 Pillow 的路径用字符串形式避免不同版本的路径对象支持差异first.save(str(out_path), PDF, save_allTrue, append_imagesrest,resolution150.0)print(f已写出 {out_path}共 {len(pages)} 页)return 0if __name__ __main__:sys.exit(main())几个必须注意的点写之前先确保输出目录存在否则save会因路径不存在而失败PDF 输出会重新编码图片不是把原始文件「打包」进 PDF所以像素质量会受页面尺寸与保存参数影响如果需求更复杂文字排版、矢量元素、水印、书签reportlab这类专门的 PDF 库会合适得多——记得它同样是第三方依赖且 API 与 Pillow 不同具体用法以该库官方文档为准。环节用到的能力来自哪里参数解析argparse.ArgumentParser、add_argument标准库路径处理pathlib.Path、is_file、mkdir标准库错误报告sys.stderr标准库读图 / 缩放 / 居中Image.open、convert、thumbnail、paste、Image.newPillow第三方多页 PDF 写入Image.save(..., save_allTrue, append_images[...])Pillow第三方常见坑点1. 不说清依赖读者一跑就报错。❌ 代码顶部只有from PIL import Image没有任何安装说明。 ✅ 明确写出「需先 pip install Pillow」并说明它是第三方库、不在标准库里。2. 一次性把所有图片读进内存再处理。❌ 先把上百张原图全部Image.open()存成列表内存瞬间吃满。 ✅ 逐张打开、缩到位、贴成页面原图句柄用完即释放with。3. 一张坏图让整批任务崩掉。❌ 循环里不捕获异常遇到损坏文件直接Traceback退出前面处理的全白做。 ✅ 逐张 try / except 捕获OSError跳过并打印警告继续处理。4. 输出目录不存在导致保存失败。❌ 直接save(输出/子目录/a.pdf)路径不存在时抛异常。 ✅ 保存前用Path(...).parent.mkdir(parentsTrue, exist_okTrue)建目录。5. 手写sys.argv解析。❌ 用下标取参数遇到-o缺少值、传了非整数就崩。 ✅ 用argparse它会自动处理短名 / 长名、类型转换与帮助信息。6. 以为 PDF 是「无损打包」。❌ 认为把原图塞进 PDF 后画质一定和原图一样。 ✅ 多页 PDF 保存会重新编码图片页面上是渲染后的位图关心画质就调大页面尺寸或换专门的 PDF 库。7. 打开图片却忘了关闭。❌im Image.open(path)在循环里反复执行文件句柄越积越多。 ✅ 用with Image.open(path) as im:缩略与粘贴都在这个上下文里完成。8. 没有给「一页都没成功」的情况留出口。❌ 全部图片都失败时仍去保存产出一个空文件或抛难以理解的异常。 ✅ 收集完页面后先判断列表是否为空为空则打印明确提示并返回非零状态码。总结步骤做什么关键点定义接口输入若干路径、输出 PDF、可选页宽 / 页高行为先讲清再写代码解析参数argparse定义位置参数与选项类型转换、默认值、自动帮助处理单张thumbnail 居中 白底保持比例、只缩不放批量收集遍历、校验路径、捕获OSError坏图跳过不中断整批写出 PDFsave(..., save_allTrue, append_images[...])Pillow 为第三方依赖写前建目录一句话收尾这个小程序的价值不在于「把图拼成 PDF」这件事本身而在于它把命令行工具的四件事——参数、路径、批处理、错误容忍——串成了一条完整链路把 PDF 写入换成任何第三方库这套骨架都不用改。