
说起图片转 PDF第一反应基本都是“这还用专门找工具”手机相册、WPS、微信自带功能都能转。可真到了要交材料、打印归档、发合同、存长截图的时候问题一个接一个长图被硬塞进一页字比蚂蚁还小一张几千像素高的截图被强行均分内容正好断在标题中间多张图片合成一个 PDF 后页面忽大忽小打印出来边距乱七八糟。我过去几年处理截图和长图转 PDF 的频率很高试用过不少免费在线工具和桌面软件最后干脆自己写了个 Python 小脚本专门做长图自动分页。今天把这套完整逻辑、脚本代码和踩过的坑都整理出来需要的人可以直接照着抄。1. “排版错乱”到底在乱什么三种典型的翻车现场先说清楚问题本身。排版错乱不是玄学背后根因都很具体不搞清楚就从工具 A 换到工具 B大概率还是会翻车。1.1 翻车现场一整张长图被压进一页 A4字变成蚂蚁这是最普遍的翻车形式。你有一张 1080×19200 像素的聊天记录截图宽高比接近 1:17而 PDF 默认页面 A4 只有 210×297 毫米。很多工具的处理逻辑是“把图片完整放进一页”那就只能按宽度等比例缩放于是整张图被压成高度只有大约 2333 毫米的一页纸——注意这个高度其实是超出 A4 的但工具会继续缩小到“刚好容纳”最后图片物理尺寸变成接近 190×3378 毫米而 A4 页面才 297 毫米高所以它会等比缩小到 16×283 毫米左右3000 多像素的高度被塞进不到 300 毫米字自然就成了像素马赛克。我最早用手机自带“打印”功能转长截图出来的 PDF 就是这样放大看全是糊的缩小看整页是密密麻麻的灰条完全没法用。这种问题的本质是工具选择了“保证完整性”但牺牲可读性。1.2 翻车现场二自动分页但切断位置随心所欲有些工具稍微聪明一点知道长图需要分页。但它们的策略往往是“按固定高度均分”不管图片内容哪里是文字、哪里是空白直接一刀切。结果就是聊天记录里的一个气泡被拦腰截断公众号文章的一行字只剩上半截电商详情页的商品图被切成两半。为什么会有这种切断因为图片在软件眼里就是像素矩阵没有语义信息。第 8000 行像素恰好在文字中间还是行间空白对程序来说完全随机。那些不做任何智能处理的工具自然就是“均分”了事。这也是网上大量免费转换工具评分低的核心原因——它们能分页但没有质量。1.3 翻车现场三边距、方向和画质一起出问题第三种翻车更隐蔽转出来的 PDF 单独看每页都还行打印出来或者发给别人后问题全暴露了。常见表现有三类扫描件或手机截图自带黑边、灰边嵌入 PDF 后页面四边有难看的黑框打印浪费墨。横图竖图混排时页面方向没有自动适配竖版长图被塞进横向页面横版全景图又被塞进竖向页面。DPI 设置不对PDF 在阅读器里用“实际大小”查看时页面物理尺寸偏大或偏小打印预览和屏幕显示严重不一致。这第三个问题很多人没意识到是 DPI 引起的总以为文件坏了。其实 PDF 页面里图片嵌入时是有物理尺寸的能否正确缩放显示取决于保存 PDF 时有没有写入正确的分辨率信息。在线工具往往为了压缩体积把这部分信息写得很随意本地软件又不一定允许你调。所以核心矛盾就一句话A4 页面尺寸固定而长宽比和图片内容不固定工具默认策略不代表你的需求。明白这点之后我就不再到处找现成工具了直接自己控制整个转换过程。2. 现成工具看起来不少为什么我最后选择自己写不是我没试过工具而是试了一圈每个都不太符合“长图自动分页”这个核心需求。下面是我实际用过的几类方案给大家一个参考。2.1 LibreOffice DrawLibreOffice Draw 可以插入图片、自由排版、导出 PDF功能其实很强。但你插入一张长图它默认也是按页面比例缩放不会自动拆页。你要手动拖动、复制多个画页、调整每页内容逐张手动切分。处理三五张图可以处理几十张聊天截图就会崩溃。而且 Draw 的 PDF 导出对中文环境有字体渲染差异偶尔会出现文字部件混乱。这工具适合做“单图精细排版”不适合批量自动分页。2.2 ImageMagick 的 convert 命令ImageMagick 是老牌命令行图像工具convert a.png b.png out.pdf能把多张图片合成一个 PDF。但它做的也是“每张图一页”面对一张 1.7 万像素高的长图默认依然压缩到一页或按照它自己的策略切片。虽然可以用-page、-resize等参数强行控制但分页逻辑要自己写和直接写 Python 脚本没有本质区别而且它的 Windows 安装、PATH 配置对新人不友好规则复杂维护成本高。Regularly 用下来不划算。2.3 在线转换网站在线工具看着方便上传-转换-下载三步搞定但问题不少。首先免费额度通常限制文件大小长图高分辨率动不动十几兆传不上去其次即使传上去服务端经常对图片二次压缩输出 PDF 的分辨率明显下降最后也是最重要的聊天记录、合同扫描件、身份证复印件这类图片我完全不放心传到第三方服务器。隐私风险是硬伤再方便也不能用。2.4 系统自带和阅读器“打印成 PDF”手机相册、Windows 照片查看器、浏览器的“打印为 PDF”其实也是常见方案。它们的共同特点是零学习成本但对分页策略没有控制权。浏览器打印网页时会按 CSS 分页规则切割手机上处理长截图则往往一页塞满。只转一两张图没问题一旦涉及批量、排版、打印参数流程不可控每次效果都可能不一样。2.5 自己写脚本控制每一个参数在把这些工具都试过之后我意识到自己需要的其实非常简单一个免费、离线、能对任意长宽比图片做自动分页的转换器。理论上用 Python 的 Pillow 库几十行代码就能搞定那么自己写就是最优解。下面这张表是我做过的小结方案长图自动分页输出画质控制批量处理离线隐私安全学习成本手机自带/打印为 PDF弱弱弱高低LibreOffice Draw弱中弱高中ImageMagick中需手写规则中中高中高在线转换网站中弱弱低低Python Pillow 脚本强强强高中选型逻辑很简单核心需求是“长图自动分页 可控质量和边距”从需求倒推方案而不是从工具的功能倒推能不能用。脚本虽然有学习成本但掌握之后批量、参数、隐私、输出质量全部自己说了算一劳永逸。3. img2pdf.py一条命令完成长图自动分页直接上脚本。这是我自己日常在用的版本基于 Python 3 和 Pillow没有任何额外依赖。核心功能有三个单张或多张图片转 PDF、长图按页面可用高度自动分页、切割线带像素级微调。# -*- coding: utf-8 -*- img2pdf.py —— 图片转 PDF 小工具支持长图自动分页。 典型用法 python img2pdf.py 截图.png # 单张长图转 PDF python img2pdf.py 截图目录 -o 合并.pdf # 文件夹内所有图片按文件名合并 python img2pdf.py 截图.png --seam 16 --dpi 144 # 切割线容差 16 像素DPI 144 import argparse import math import os from PIL import Image PAGE_W_MM 210.0 PAGE_H_MM 297.0 def prepare_image(path): 打开图片并统一为 RGB透明背景填充成白色。 img Image.open(path) 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]) img bg else: img img.convert(RGB) return img def row_diff(img, y): 计算第 y-1 行和第 y 行之间的像素差异用于判断切割线是否落在内容间隙。 w, h img.size if y 0 or y h: return 0 row_a list(img.crop((0, y - 1, w, y)).getdata()) row_b list(img.crop((0, y, w, y 1)).getdata()) return sum(abs(a - b) for a, b in zip(row_a, row_b)) def pick_cut_y(img, cut, seam): 在 cut±seam 像素范围内找相邻行差异最小的位置作为实际切割线。 lo, hi max(1, cut - seam), min(img.height - 1, cut seam) best_y, best_score cut, float(inf) for y in range(lo, hi 1): score row_diff(img, y) if score best_score: best_y, best_score y, score return best_y def split_long_image(img, avail_w_mm, avail_h_mm, dpi, seam): 核心分页逻辑先统一图片宽度再按可用页高计算页数并切片。 target_w max(1, round(avail_w_mm / 25.4 * dpi)) if img.width ! target_w: new_h round(img.height * target_w / img.width) img img.resize((target_w, new_h), Image.LANCZOS) avail_h_px max(1, round(avail_h_mm / 25.4 * dpi)) if img.height avail_h_px: return [img] pages math.ceil(img.height / avail_h_px) per_page img.height / pages frames [] prev 0 for i in range(pages): cut min(int(round((i 1) * per_page)), img.height) if i pages - 1: cut pick_cut_y(img, cut, seam) frames.append(img.crop((0, prev, img.width, cut))) prev cut return frames def pad_to_same_height(frames): 把不同高度的帧统一补白到相同高度保证 PDF 所有页面尺寸一致。 max_h max(f.height for f in frames) result [] for f in frames: if f.height max_h: padded Image.new(RGB, (f.width, max_h), (255, 255, 255)) padded.paste(f, (0, 0)) result.append(padded) else: result.append(f) return result def convert_files(files, out_pdf, margin_mm, dpi, seam): frames [] avail_w_mm PAGE_W_MM - 2 * margin_mm avail_h_mm PAGE_H_MM - 2 * margin_mm for path in files: img prepare_image(path) frames.extend(split_long_image(img, avail_w_mm, avail_h_mm, dpi, seam)) frames pad_to_same_height(frames) if not frames: raise SystemExit(没有可写入 PDF 的图片) frames[0].save( out_pdf, PDF, save_allTrue, append_imagesframes[1:], resolutiondpi, ) print(f已生成 {out_pdf}共 {len(frames)} 页) def main(): parser argparse.ArgumentParser(description图片转 PDF支持长图自动分页) parser.add_argument(input, nargs, help图片路径或目录) parser.add_argument(-o, --output, defaultoutput.pdf) parser.add_argument(-m, --margin, typefloat, default10.0) parser.add_argument(--dpi, typeint, default96) parser.add_argument(--seam, typeint, default8) args parser.parse_args() files [] for item in args.input: if os.path.isdir(item): for name in sorted(os.listdir(item)): if name.lower().endswith((.png, .jpg, .jpeg, .webp, .bmp)): files.append(os.path.join(item, name)) else: files.append(item) if not files: raise SystemExit(没找到可用图片) convert_files(files, args.output, args.margin, args.dpi, args.seam) if __name__ __main__: main()使用前先确认环境pip install pillow。然后把脚本保存为img2pdf.py在命令行里执行即可。3.1 分页算法是怎么设计的脚本的核心是split_long_image函数。它做的事情分三步第一步统一图片宽度。A4 宽度 210 毫米减去你设置的两侧边距默认各 10 毫米得到可用宽度 190 毫米。根据 DPI 换算成目标像素宽度把图片等比缩放到这个宽度。这样所有图片在 PDF 里的物理宽度完全一致页面整体整齐。第二步按可用高度算页数。可用高度 297 - 2×10 277 毫米换算成像素。如果图片高度小于这个值就直接一页放完如果大于页数 向上取整图片高度 ÷ 页面可用高度。这一步解决的问题就是“图片太长不能一页塞下”。第三步切割线微调。这是和普通工具拉开差距的关键点。默认的切割位置是“均分”也就是固定高度一刀切。但实际图片里第 8000 行正好落在文字行中间切割出来就会断字。pick_cut_y函数会在默认切割点上下各seam像素默认 8 像素范围内寻找“相邻两行像素差异最小”的位置作为实际切割线。像素差异小的地方往往是行间空隙或空白区域切割线滑到那里文字被切断的概率就明显降低。3.2 为什么 DPI 会影响排版DPI 看着像打印相关参数其实也影响屏幕显示。脚本里resolutiondpi会把 DPI 信息写入 PDF阅读器按这个值换算物理尺寸。默认 96 适合屏幕阅读打印建议--dpi 150需要印刷质量可以--dpi 300。如果转出来的 PDF 在“实际大小”视图下页面偏大或偏小多半就是 DPI 问题重新生成即可。4. 三张真实截图实测从裁掉半边字到干净分页脚本写完之后我拿三张真实的、以前翻过车的图测了一遍效果分别说一下。4.1 案例一1080×19200 的微信聊天记录截图这张截图是 1080 像素宽、19200 像素高接近 17.8:1 的超长竖图。以前用手机自带功能转出来只有一页全图挤在一起放大看字全是糊的。用脚本默认参数跑一遍可用宽度 190 毫米DPI 96换算后图片显示高度 190 × (19200 ÷ 1080) ≈ 3378 毫米可用高度 277 毫米页数 ceil(3378/277) ≈ 13 页。实测生成 13 页每页宽度相同内容从上到下连续聊天记录里的每条消息完整可读。切割线基本都落在消息之间的空隙处少数几张正好切在图片边缘也没有断字。4.2 案例二公众号长文截图顶部有封面横幅公众号文章截图通常第一屏是标题、封面横幅、引导关注图下面才是正文长图。自动分页如果直接均分封面横幅很可能被切掉一半观感很差。我的处理技巧是把封面图和正文图分开处理。封面单独转一页 PDF正文长图再交给脚本自动分页。实际操作两条命令python img2pdf.py 封面图.png -o part1.pdf python img2pdf.py 正文长图.png -o part2.pdf如果只有一张完整长图但前 800 像素是封面也可以先在图片处理软件里把前 800 像素裁出来单独保存再分别转换。虽然多两步但结果完全是可控的。自动分页只能做到“像素级不切断”做不到“语义级不切断”关键内容该拆还是拆出来手动处理。4.3 案例三电商详情页长图包含参数表格和商品图电商详情页长图一般由商品图、卖点图、参数表格拼接而成中间有很多不同背景色块。这种图用默认seam8基本够用但遇到表格区域时均分切割点可能落在表头或表行内部。我的做法是调大 seam 到 16让切割线在更大的范围内滑动更容易滑到行与行之间python img2pdf.py 详情页.png --seam 16实测下来表格区域被切中行的概率明显下降。但要注意seam 不能无限调大否则本属于前一页的内容会被挪到后一页页面内容比例失衡。我一般控制在 8 到 20 之间超过 20 就得不偿失。5. 除了长图切片这些细节让脚本真正好用脚本满足“长图自动分页”之后我又补充了几个小功能现在日常使用基本不离手。5.1 批量和文件命名排序脚本天然支持目录作为输入img2pdf.py 截图目录会按文件名排序后合成一个 PDF。批量场景下建议把文件名提前命名成01_内容.png、02_内容.png这种带序号的形式。因为排序是按字符串排序2.png会排在10.png后面序号前补零可以避免顺序乱掉。我处理过一批 40 张合同扫描件命名成page_01到page_40一条命令生成 40 页 PDF顺序完全正确。5.2 透明背景图片的处理很多网页截图、设计稿导出图是带透明通道的 PNG。如果直接把 RGBA 模式图片写入 PDF透明区域在部分阅读器里会变成黑色块。prepare_image函数里专门做了处理检测到 RGBA、LA、Palette 模式时先转成 RGBA再粘贴到白色背景上最后统一转 RGB。这样透明区域全部变成白底视觉上干干净净。5.3 超大图片的内存问题长图如果超过 30000 像素高比如超长网页截图或者设计长图原图Pillow 全图解码会占用较多内存低配电脑可能卡顿。两个思路一是先做降采样把图片缩到 1500 像素宽左右再转 PDF画质虽然降一点但大多数阅读场景完全够用二是分段裁剪先按高度切成几段再分别转 PDF最后用 PDF 合并工具拼起来。我日常碰到超长图都是用第一种简单粗暴且稳定。5.4 灵活使用 --seam 参数前面提过 seam 的作用是让切割线“滑”到行间空隙。建议初次用默认 8转完翻一遍 PDF。如果发现某页切割线正好压在文字中间就针对这张图加--seam 16重新生成。记住这个参数只影响切割线微调幅度不影响图片缩放和页面尺寸。6. 图片转 PDF 路上的经典坑以及我的兜底办法最后把这几年的踩坑经验汇总成一张表都是日常高频问题。问题典型表现原因解决办法黑边/灰边扫描件或截图四边有深色边框原图自带未裁剪转 PDF 前先做边缘裁剪或先填充白底中文文件名乱码命令行无法打开图片Windows 编码与 Python 默认编码不一致用英文/拼音命名文件输出路径也用英文DPI 不一致打印预览尺寸不对保存 PDF 时缺少分辨率信息转 PDF 时指定--dpi 150或300PDF 体积巨大几十页 PDF 达到几百 MB原图分辨率过高且未压缩先降采样或把 DPI 改为 96 再转横竖混排横版全景图被塞进竖页页面方向固定为 A4 竖向横图单独转横向页面再合并内容被切断文字行被切成两半均分切割线落在行中调大--seam或手动提前裁出关键区块内存不足超长图转换卡死图片像素总量过大先缩到 1500 像素宽再转或分段处理有几点值得展开说说。第一个是打印前务必预览“实际大小”。我遇到过同事拿着转好的 PDF 直接打印结果出来页面明显缩小白边巨大。原因就是当初在手机/在线工具里转的时候 DPI 信息不对PDF 引擎按错误物理尺寸渲染。自己脚本控制 DPI 之后这种情况基本绝迹。第二个是 PDF 体积控制。高分辨率手机截图直接转 PDF动辄几十 MB发微信会被压缩。我的经验是把 DPI 降到 96配合 JPEG 压缩Pillow 存储时会自动处理体积能缩到原来的三分之一屏幕阅读没有任何区别。如果是打印用途再单独用 150 DPI 生成一版两版分开存。第三个是关于“切割线微调”的边界。row_diff计算的是相邻两行的像素差对灰度或彩色图片都有效但它只能判断“两行像不像”不能理解“这里是不是文字”。当图片内容是一整块连续图表、带连续边框的表格时哪怕 seam 调得再大也很难找到真正的语义切割点。真遇到这种情况我通常手动把图表区域单独裁出来转一页其他部分再自动分页反而是最快的方案。我自己用到现在的感受是图片转 PDF 这件事不一定需要装一个几百兆的软件也不一定需要把图片传到别人的服务器上。一个本地的、几十行的小脚本能自动分页、能控制边距、能调整画质基本覆盖了 90% 的使用场景。现在每次遇到长图转 PDF我都是直接拖进命令行根据这张图的内容特点把 seam 和 DPI 调一下几秒钟出一版干净的 PDF。如果你也在被长图排版问题折磨建议先装个 Python 和 Pillow把上面的脚本存下来试一次。后面想再偷懒一点还可以把常用参数包装成一个双击执行的批处理脚本拖入图片一键出 PDF这就是另一个话题了。