
前阵子接了一个有点烦人的活儿公司一个项目归档合作方内部系统只收 JPG 图片几十份 Word 合同、方案文档要一份份转成高清图片发过去。我一开始想省事让同事直接截图。结果截图文件大小参差不齐有些多页文档滚动拼接还错位图片放大一点就糊成一片。后来我定了两条技术路线分别写成两个 Python 脚本——一个在 Windows 上直接调用 Office 引擎一个在 Linux 服务器上用 LibreOffice 兜底把 .doc 和 .docx 统一转成高质量 JPG。这篇就把我的实现思路、完整代码、调试过程中踩过的坑一次说完。整个过程的核心思路其实不复杂Word 本身没有“导出为 JPG”的功能所以不能硬来要走一条相对稳定的转换链路——先用文档引擎把 Word 转成 PDF再把 PDF 按页光栅化成高清 JPG。这个思路解决了两个问题排版保真、清晰度可控。如果你也遇到过类似“必须把 Word 变图片”的需求或者你想在自己的工具链里加一个文档转图的小脚本这篇应该能帮你省掉不少弯路。1. Word 转图这件事为什么不能靠“截图”解决1.1 截图方案的四宗罪很多人都琢磨过“直接截图不就行了”我最初也这么想过。但真操作起来截图方案的问题非常明显第一分辨率不可控。屏幕截图是像素级的Word 页面显示多大就截多大。屏幕分辨率一般是 1920×1080 或 2560×1440无论你怎么放大截出来的是同一份像素数据。一旦原始页面很长你还得滚动多次再拼接拼接处稍微错位就出现横线阅读体验极差。第二字体渲染不稳定。不同电脑的 Word 字体配置不同截图时如果本机缺了文档里的某个字体Word 会临时用替代字体渲染肉眼看上去可能差不多但图片里的字形细节和原文档有差异。万一对方拿着图片去做文字比对或者打印出来看这种差异还挺致命。第三多页管理混乱。几十页的 Word 要截几十张图文件命名靠手动漏截、重复截基本无法避免。更别提有些页面有页眉页脚、批注、修订痕迹截出来后这些元素会“漂”到不该出现的位置。第四放大必糊。截图的本质是屏幕像素快照截完放大 200% 就全是锯齿。而“高清”的核心诉求恰恰是放大看也清晰。1.2 真正能用的转换链路Word → PDF → JPG既然不能直接截就必须找一个中间格式。我在对比过几种技术路线之后锁定了 PDF 作为中间层。原因很直接PDF 是矢量格式页面尺寸、字体、分页位置都是固定的。Word 文档转成 PDF 后相当于做了一次“排版定稿”每一页的内容、位置、大小都被锁定。之后再把这个矢量 PDF 光栅化成位图 JPG渲染引擎可以按任意 DPI每英寸像素数输出清晰度完全掌握在自己手里。用一句话概括PDF 是印刷版JPG 是照片。你拿印刷版去拍照想拍多清晰都行但你直接拍屏幕就只能得到屏幕的分辨率。有人可能会问能不能直接用 python-docx 之类的库去解析 .docx 然后自己画图我的回答是不要想。python-docx 只能操作 docx 里底层的 XML 结构它能帮你改文字、改样式、加表格但没法复现 Word 完整排版引擎的行为。分页符、文本框、目录、公式、批注、页眉页脚这些一旦组在一起自己做渲染就是无底洞。复用现成文档引擎才是工程上的正确方向。2. 脚本一Windows 上调用 Office 引擎实现“所见即所得”2.1 环境准备少一步都不行第一个脚本的运行环境很明确Windows 已安装 Microsoft Office。它走的是 win32com 通道也就是让 Python 通过 COM 接口“远程遥控”本机 Word 应用程序。需要安装的 Python 依赖有两个pip install pypiwin32 pymupdfpypiwin32通常也叫pywin32提供 Windows COM 调用能力pymupdf模块名fitz负责把 PDF 渲染成高清图片。额外的隐藏条件是系统里必须装了 Office并且 Word 应用能正常打开。Office 2016、2019、365 的 COM 接口行为基本一致我测试下来没有遇到版本差异问题。2.2 完整代码word2jpg_win.py这个脚本我写成了命令行工具支持指定输入文件和输出目录# -*- coding: utf-8 -*- word2jpg_win.py Windows Office 环境下将 .doc/.docx 转为高质量 JPG 依赖: pip install pypiwin32 pymupdf 用法: python word2jpg_win.py input.docx --out ./output --dpi 300 import os import time import argparse import win32com.client import fitz # PyMuPDF def doc_to_pdf(doc_path, pdf_path): 通过 Word COM 接口把文档导出为 PDF。 wdExportFormatPDF 的枚举值是 17。 word win32com.client.DispatchEx(Word.Application) word.Visible False word.DisplayAlerts 0 try: doc word.Documents.Open( doc_path, ConfirmConversionsFalse, ReadOnlyTrue, AddToRecentFilesFalse, ) doc.ExportAsFixedFormat(pdf_path, 17) doc.Close(False) finally: word.Quit() def pdf_to_images(pdf_path, out_dir, dpi300): os.makedirs(out_dir, exist_okTrue) pdf fitz.open(pdf_path) base os.path.splitext(os.path.basename(pdf_path))[0] for page_idx in range(pdf.page_count): page pdf.load_page(page_idx) # PDF 坐标系默认是 72 DPI要输出 dpi需要放缩 dpi / 72 倍 matrix fitz.Matrix(dpi / 72, dpi / 72) pix page.get_pixmap(matrixmatrix, alphaFalse) out_file os.path.join(out_dir, f{base}_第{page_idx 1:02d}页.jpg) pix.save(out_file) print(f[OK] {out_file}, 尺寸: {pix.width}x{pix.height}) pdf.close() return pdf.page_count def convert_word_to_jpg(doc_path, out_diroutput, dpi300): if not os.path.exists(doc_path): raise FileNotFoundError(f文件不存在: {doc_path}) os.makedirs(out_dir, exist_okTrue) pdf_path os.path.join(out_dir, os.path.basename(doc_path) .pdf) print(f[1/2] 正在调用 Word 导出 PDF: {pdf_path}) doc_to_pdf(doc_path, pdf_path) # 刚生成的文件偶尔会有写入延迟等待 1 秒再读取 time.sleep(1) print(f[2/2] 正在渲染高清 JPGDPI{dpi}) page_count pdf_to_images(pdf_path, out_dir, dpidpi) print(f转换完成共 {page_count} 页) if __name__ __main__: parser argparse.ArgumentParser(descriptionWord 转 JPG 高清转换工具Windows) parser.add_argument(input, help输入的 Word 文件 (.doc / .docx)) parser.add_argument(--out, defaultoutput, help输出目录默认 output) parser.add_argument(--dpi, typeint, default300, help输出 DPI默认 300) args parser.parse_args() convert_word_to_jpg(args.input, args.out, args.dpi)调用方式python word2jpg_win.py 项目合同.docx --out ./images --dpi 3002.3 几个值得注意的参数细节wdExportFormatPDF枚举值是 17。这个 17 不是随手写的魔法数字而是 Word 内部对 PDF 导出格式的定义。你写成doc.ExportAsFixedFormat(pdf_path, 17)等价于在 Word 里手动“另存为 PDF”。DispatchEx而不是Dispatch这点非常重要。Dispatch会连接到一个已有的 Word 实例如果你电脑上正好开着别的 Word 文档脚本可能会操作到同一个进程轻则互相干扰重则把你的文档动到。DispatchEx会创建一个独立的 Word 进程脚本跑完只关掉自己这个不影响用户正在编辑的文档。我第一次写的时候用的Dispatch在一次批量转换中差点把同事没保存的文档关掉后来就再也不敢用Dispatch了。word.DisplayAlerts 0用来抑制 Word 弹窗。没有这行的话遇到“文档正在使用中”“是否保存更改”之类的提示脚本会直接卡住等用户点击而在VisibleFalse的情况下你根本看不到那个弹窗。ReadOnlyTrue是防止转换时误改动原始 docx 文件的元数据。这个习惯建议保持防患于未然。2.4 “高清”到底是多高先把 DPI 聊透很多朋友对 DPI 没有直观概念我这里给一个参考公式。PDF 默认的坐标基准是 72 DPI也就是说在 PDF 里1 英寸对应 72 个逻辑单位。PyMuPDF 渲染时用Matrix(dpi / 72, dpi / 72)把逻辑单位放大到目标 DPI 对应的像素数。A4 纸宽度是 210 毫米约 8.27 英寸。那么150 DPI 时一页 A4 的横向像素 8.27 × 150 ≈ 1240 像素300 DPI 时一页 A4 的横向像素 8.27 × 300 ≈ 2480 像素600 DPI 时一页 A4 的横向像素 8.27 × 600 ≈ 4960 像素。300 DPI 基本是打印领域的标准清晰度屏幕上查看已经非常锐利。如果你需要放大看微小文字可以用 600 DPI如果只是微信群传阅150 DPI 就足够。脚本里默认 300适合大多数归档场景。3. 脚本二跨平台版用 LibreOffice 做无 Office 兜底3.1 为什么还需要第二个脚本第一个脚本很香但有一个硬伤它依赖 Windows Office。我这边有一台跑批量任务的 Linux 服务器上面不可能装 Microsoft Office也没有图形界面。如果每次都要把 Windows 生成的文件传上去自动化链路就断了一半。LibreOffice 是开源的办公套件它提供--headless无头模式可以在服务器上只靠命令行完成 Word 到 PDF 的转换纯命令行、无需 GUI、跨平台。虽然渲染保真度不如 Word 原生引擎那么完美但应对日常文档转换已经够用。3.2 安装配置注意补中文字体以 Ubuntu/Debian 为例sudo apt update sudo apt install -y libreoffice-writer fonts-noto-cjklibreoffice-writer负责处理 Word 文档fonts-noto-cjk是思源黑体的系统包。如果不装中文字体转换出来的 PDF 里中文会变成一个个“豆腐块”或者乱码方框这个坑我后面专门说。如果要批量转换也可以直接用命令行soffice --headless --convert-to pdf --outdir ./pdf_build ./input/*.docx这条命令会把./input目录下的所有 docx 文件转换成 PDF输出到./pdf_build。3.3 Python 整合脚本word2jpg_cross.py实际项目中我不太喜欢在 shell 层面做太多处理更喜欢用 Python 统一调度。下面这个脚本会遍历一个目录里的所有 .doc/.docx先交给 LibreOffice 转 PDF再用 PyMuPDF 渲染成 JPG# -*- coding: utf-8 -*- word2jpg_cross.py 跨平台方案LibreOffice PyMuPDF把 Word 转 JPG 依赖: sudo apt install -y libreoffice-writer fonts-noto-cjk pip install pymupdf 用法: python word2jpg_cross.py ./doc_dir --out ./images --dpi 300 import os import sys import time import glob import argparse import subprocess import fitz # PyMuPDF def convert_doc_dir(doc_dir, out_dir, dpi300): os.makedirs(out_dir, exist_okTrue) doc_files glob.glob(os.path.join(doc_dir, *.doc)) \ glob.glob(os.path.join(doc_dir, *.docx)) if not doc_files: print(f在 {doc_dir} 下没找到 .doc/.docx 文件) return for doc_file in doc_files: print(f处理: {doc_file}) # 1. LibreOffice 转 PDF subprocess.run( [ soffice, --headless, --convert-to, pdf, --outdir, out_dir, doc_file, ], checkTrue, capture_outputTrue, ) # 2. 渲染 PDF 为 JPG base os.path.splitext(os.path.basename(doc_file))[0] pdf_path os.path.join(out_dir, base .pdf) pdf_to_images(pdf_path, out_dir, dpi) # 3. 可选删除中间 PDF保留 JPG os.remove(pdf_path) def pdf_to_images(pdf_path, out_dir, dpi300): pdf fitz.open(pdf_path) base os.path.splitext(os.path.basename(pdf_path))[0] for page_idx in range(pdf.page_count): page pdf.load_page(page_idx) matrix fitz.Matrix(dpi / 72, dpi / 72) pix page.get_pixmap(matrixmatrix, alphaFalse) out_file os.path.join(out_dir, f{base}_第{page_idx 1:02d}页.jpg) pix.save(out_file) print(f[OK] {out_file}, 尺寸: {pix.width}x{pix.height}) pdf.close() return pdf.page_count if __name__ __main__: parser argparse.ArgumentParser(descriptionWord 转 JPG 高清转换工具跨平台) parser.add_argument(doc_dir, help存放 Word 文件的目录) parser.add_argument(--out, defaultoutput, help输出目录默认 output) parser.add_argument(--dpi, typeint, default300, help输出 DPI默认 300) args parser.parse_args() convert_doc_dir(args.doc_dir, args.out, args.dpi)这段脚本的核心就是subprocess.run调soffice。因为参数是通过列表传的不做 shell 拼接所以即使路径里有中文或空格也不会出问题。3.4 两套方案的差异对比对比维度脚本一win32com脚本二LibreOffice运行平台仅 WindowsWindows / Linux / macOS必须安装Microsoft OfficeLibreOffice转换保真度与 Word 原生渲染一致接近复杂排版可能有细微差异.doc 旧格式支持支持批量并发不建议并发可以串行批量字体依赖系统已装字体需要手动补中文字体适用场景个人电脑、少量高保真转换服务器批量转换选型建议很直接如果你只是本地偶尔转几份文档脚本一就够了如果你要部署到服务器做自动化流水线优先考虑脚本二。4. 影响高清效果的三个关键参数与实际调试经验4.1 DPI不是越大越好DPI 是“每英寸像素数”很多人有个误区觉得 DPI 越高越好直接冲到 1200。但 DPI 翻倍意味着图片体积翻四倍渲染时间也成倍增加而肉眼在屏幕上超过 300 DPI 之后几乎分辨不出差别。我的选择标准是150 DPI微信群传阅、快速预览、OA 系统上传文件小、传输快300 DPI文档归档、打印查看、OCR 识别清晰度和体积的平衡点600 DPI需要放大看微小文字或印刷级复核时使用日常不推荐。4.2 JPEG 质量与 PNG 的取舍脚本默认输出 JPG因为文件体积小、通用性最好。但 JPG 是有损压缩白底黑字这种高对比内容在低质量参数下文字边缘会出现灰色晕染俗称“毛边”。如果对清晰度有极致要求我会建议二选一JPGpix.save()默认质量已经不低保守起见可以用 90 以上PNG完全无损文字边缘锐利但文件体积会大不少适合不用被系统限制格式的场景。PyMuPDF 的get_pixmap拿到的是像素对象如果想精确控制 JPEG 质量可以先把图片转成字节再写文件jpg_data pix.tobytes(outputjpg, jpg_quality95) with open(out_file, wb) as f: f.write(jpg_data)我测试过质量 95 和默认值肉眼差别不大但设置成 90 时会明显看到文字边缘发灰。所以归档类场景宁可文件大一点也建议把质量参数拉到 95。4.3 字体缺失是最大的“隐形翻车点”这一点我必须单独拎出来讲因为它直接决定了转换结果是否“能用”。Windows 上跑脚本一因为系统里装了全套 Office 字体文档里用的“微软雅黑”“宋体”“Calibri”都能正确渲染。但同样的文档放到 Linux 服务器上用 LibreOffice 转如果系统没装对应字体LibreOffice 就会用自己的替代字体。替代的结果一般是两类一是行宽变化、表格错位、文字溢出二是中文直接变成豆腐块。排查方法也不难。先看系统里有哪些中文字体fc-list | grep -i cjk如果没有输出就说明系统一个中文字体都没有必须先安装。Debian/Ubuntu 安装sudo apt install -y fonts-noto-cjk安装后刷新字体缓存fc-cache -f再跑一次转换中文就正常了。还有一个进阶技巧转完 PDF 后可以用pdffonts命令查看 PDF 里到底嵌入了哪些字体pdffonts 合同.pdf如果发现字体一栏写着NotoSansCJK-Regular说明系统已经在用 Noto 替代了。如果你希望文档里原字体名被保留需要在系统里安装与文档使用字体同名的字体不过这通常比较难做到完全一致实务上只要版式不乱、中文正常就可以接受。4.4 空白页与封面自动清理才是完整方案Word 文档经常在末尾多出一个空白页或者中间夹着分页符导致的空页。转 PDF 后这些空白页也会变成全白 JPG既浪费存储又影响阅读。我加了一个全白页检测函数在渲染完成后自动判断def is_white_image(img_path, threshold245): 判断图片是否接近全白threshold 越接近 255 越严格 from PIL import Image img Image.open(img_path).convert(L) extrema img.getextrema() return extrema[0] threshold你也可以不用 PIL直接用 PyMuPDF 读 PDF 页面的文本统计如果一页没有任何文本、也没有任何图片和线条就认定为空白页跳过渲染。这个方案更轻量不用多装一个 Pillow 依赖。5. 踩坑实录从 COM 进程残留到批量卡死的完整排查链路5.1 现象脚本跑完了任务管理器里全是 WINWORD.EXE这个问题在我最初用 COM 脚本时出现过很多次。脚本运行结束任务管理器里能数出十几个 WINWORD.EXE 进程每个占用一两百 MB 内存。批处理 50 个文件电脑直接卡成 PPT。排查链路是这样的先确认脚本里有没有把 Word 进程关掉。第一版代码是这样写的doc word.Documents.Open(doc_path) doc.ExportAsFixedFormat(pdf_path, 17) doc.Close() word.Quit()表面上每一步都做了但问题出在“中途出错”的分支。如果某一次ExportAsFixedFormat抛异常后面的Close和Quit根本不会执行。Python 进程退出后COM 引用计数没有被清理Word 进程就一直挂在那。批量任务里只要有一两份文档异常进程就会累积。解决办法是把资源清理代码放进finally块try: doc word.Documents.Open(...) doc.ExportAsFixedFormat(...) doc.Close() finally: word.Quit()另外所有 COM 变量的引用计数都尽量主动清零doc None word None对于已经泄漏的进程可以对比任务管理器里新增的 PID 来精准清理而不是直接taskkill /im WINWORD.EXE误杀用户开着的文档。我后来甚至加了一个保险逻辑转换前记录系统现有的 WINWORD.EXE PID 集合转换完只杀掉本次产生的新进程。5.2 现象批量转换时“第 N 个文件”卡死没有任何报错这个坑耗费了我大半天。脚本从前几个文件跑得好好的到某个文件时突然不动了没有报错、没有输出就那么僵住十几分钟。我的排查思路是先加日志把每个文件的处理进度打印出来定位到具体是哪个文件卡住。然后手动用 Word 打开那个文件发现一打开就弹了个提示框——“是否启用宏”或者“文档来自其他设备是否以受保护视图打开”。虽然脚本里设置了word.Visible False但弹窗还是会出现只不过你看不到。Word 在等待用户交互脚本自然就卡住了。对症下药word.DisplayAlerts 0抑制一部分弹窗Documents.Open时传ConfirmConversionsFalse避免格式转换询问对于受保护视图改用本地文件路径或者把文件从网络路径先复制到本地再打开最稳妥的办法是加一个“看门狗”机制把单个文件的转换放在子进程里跑设置超时时间超时就杀掉进程继续下一个。超时包装的思路也很简单用subprocess调转换函数即可。我在第 3 节的批量脚本里就刻意用了subprocess.run调soffice除了跨平台还能天然获得超时控制subprocess.run(cmd, timeout60, checkTrue)60 秒转不完一份文档基本可以判定文档有问题杀掉继续即可。5.3 现象PDF 明明生成完毕读取时却报文件被占用或 0 字节这个问题出在 Windows 脚本里。ExportAsFixedFormat返回后我立刻用 PyMuPDF 打开 PDF偶尔会报“文件被占用”或者读取到的页数为 0。背后的原因是Office 导出 PDF 不是一个“瞬间完成”的原子操作。方法返回了但文件句柄还没有完全释放杀毒软件也在扫描新文件所以立刻打开会碰壁。解决方法很粗暴但有效——加延时和轮询time.sleep(1) for _ in range(10): if os.path.exists(pdf_path) and os.path.getsize(pdf_path) 0: break time.sleep(0.5)确认文件存在且非空之后再交给 PyMuPDF稳定度明显提升。5.4 现象Linux 上转出来的中文全是豆腐块这是一个典型的“字体缺失”问题我前面提过。根源在于 LibreOffice 在系统里找不到中文字体只能用默认的 fallback。排查方法fc-list | grep -i cjk pdffonts 转换后的pdf修复方法sudo apt install -y fonts-noto-cjk fc-cache -f但要注意装完字体后之前转出来的 PDF 已经废了需要重新转换。所以在批量任务里我习惯在转换前先做一次字体检查缺字体就直接 fail fast不浪费时间跑完整批任务再发现结果全黑。6. 从脚本到工具多页长图、自动清理与拖拽式调用6.1 把所有页面拼成一张长图很多场景下一页一张 JPG 反而不方便阅读。比如微信群里发长文件预览、公众号配长图、团队知识库展示一张连续长图会顺畅很多。我写了一个拼接函数用 Pillow 把所有 JPG 按顺序纵向拼起来# 依赖: pip install pillow import os from PIL import Image def merge_to_long_image(image_dir, output_pathmerged.jpg, max_width1200): images [] for name in sorted(os.listdir(image_dir)): if name.lower().endswith(.jpg): img Image.open(os.path.join(image_dir, name)).convert(RGB) if img.width max_width: img img.resize((max_width, int(img.height * max_width / img.width))) images.append(img) total_height sum(img.height for img in images) canvas Image.new(RGB, (max_width, total_height), white) offset 0 for img in images: canvas.paste(img, (0, offset)) offset img.height canvas.save(output_path, quality95) return output_path调用merge_to_long_image(./images, 合并长图.jpg)一步到位。6.2 递归处理整个目录如果文档分散在多个子目录glob.glob(*.docx)只匹配当前目录是不够的。改成os.walk递归遍历for root, _, files in os.walk(doc_dir): for name in files: if name.lower().endswith((.doc, .docx)): file_path os.path.join(root, name) # 转 PDF - 渲染 JPG - 清理 PDF注意输出目录和输入目录不要设成同一个否则遍历过程中会读到刚生成的 JPG导致重复处理。6.3 做成桌面拖拽工具我最后把 Windows 版脚本封装成了一个.bat文件放在桌面。使用方式变成了选中多个 Word 文件直接拖到 .bat 图标上松手自动开始转换。效果比打开命令行敲参数友好得多。.bat内容echo off chcp 65001 nul python D:\tools\word2jpg_win.py %1 --out D:\tools\output pause批量拖拽就需要改成%*遍历处理不过那又是另一个话题了。这套脚本我现在还在用每周固定跑一次把新产生的文档归档成图片版JPG 用于快速预览和群发确认PDF 版留在服务器做原始档案备份。整套流程跑下来基本告别了手动截图时代。