pptx-template-clean-rebuild
问题
用 python-pptx 打开一个自带 slide 的模板(如 PPT template.pptx),然后清空 sldIdLst 再 add_slide 新增 N 页,保存后会出现两种被严格解析器(PowerPoint / 腾讯文档预览)判为损坏的现象:
- 整份空白:zip 内
ppt/slides/slide1.xml出现两个同名条目(模板孤儿 part + 新页 part 重名)。 - 2-29 页空白,仅封面正常:
ppt/_rels/presentation.xml.rels里两条 slide 关系同时指向slides/slide1.xml(rId3孤儿引用残留 +rId11新引用)。
这两种都让 PowerPoint 拒绝渲染。而 LibreOffice 往往能渲染出内容——所以只要「LibreOffice 正常但 PowerPoint/预览空白」,几乎可断定是包结构问题,不是数据丢失。
根因
prs.slides._sldIdLst.remove(slide) 只删了 sldIdLst 里的条目,不删除 slide 的 part 与 rels。于是:
- 孤儿
ppt/slides/slide1.xml仍在 zip,与新页重名; - 孤儿关系
rId3 → slides/slide1.xml残留于 presentation.xml.rels。
修复流程(两步,缺一不可)
步骤 1:去重 zip 重名条目(保留最后一次写入)
import zipfile
path = "out.pptx"; tmp = "out_tmp.pptx"
with zipfile.ZipFile(path, "r") as zin:
infolist = zin.infolist()
last_idx = {}
for i, info in enumerate(infolist):
last_idx[info.filename] = i
keep = [i for i in range(len(infolist)) if i in set(last_idx.values())]
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
for i in keep:
info = infolist[i]
zi = zipfile.ZipInfo(info.filename, info.date_time)
zi.compress_type = zipfile.ZIP_DEFLATED
zi.external_attr = info.external_attr
zout.writestr(zi, zin.read(info.filename))
os.replace(tmp, path)
步骤 2:重写 presentation.xml.rels + presententation.xml
读取 ppt/_rels/presentation.xml.rels 与 ppt/presentation.xml:
- 解析全部
<Relationship>,区分类型为/slide与非 slide; - 非 slide 关系原样保留;
- 按
sldIdLst中<p:sldId r:id="...">的顺序,给 slide 重新分配连续且唯一的 rId,只保留这 N 条 slide 关系(剔除孤儿如 rId3); - 关键:slide 的新 rId 绝不能与非 slide 关系撞号。 非 slide 关系(
notesMaster/handoutMaster/presProps/viewProps/tableStyles/commentAuthors/tags)常占用rId32..rId38这类高位 Id——若把 slide 硬编号为rId11..rId39,会从第 22 页起与非 slide 关系 rId 冲突,导致第 22-28 页黑屏/空白、其余页正常(LibreOffice 与 PowerPoint 一致)。正确做法:slide 新 Id 从max(非slide关系数字Id) + 1开始,或对全部关系统一连续重编号并同步更新 presentation.xml 里的sldMasterId/notesMasterId等引用。 - 重建
sldIdLst(id 从 256 递增)引用新 rId。
完整可运行脚本见同目录 rebuild_clean.py(已含步骤 1+2,os.replace 原子覆盖)。
固化
把这两步封装进生成脚本的保存后处理函数(如 _finalize_package(path)),在 prs.save() 之后调用。此后一条命令即产干净成品,不再需要手动去重/清理。
验证
slide1.xml在 zip 中条目数 == 1- presentation.xml.rels 的 slide 引用去重后数量 == 页数
- sldIdLst 页数 == 页数
- LibreOffice
--convert-to pdf+pdftoppm渲染全部页,覆盖率 > 5%(无空白)
教训
凡「模板本身含 slide」再清空重排的 python-pptx 生成,必须做 rels 清理,否则 PowerPoint 严格校验必挂。判断症状:LibreOffice 渲染正常 + PowerPoint/预览空白 → 查 rels 重复 slide 引用。