PPTX 背景替换 / 装饰叠加(保留文字内容)
把模板 PPTX 的"背景视觉元素"套用到目标 PPTX 的全部页面,同时严格保持文字内容、位置、形状完全不变。
已实现两种方案:
- 方案 A(保守叠加):保留每页原有底色,在该底色之上、内容之下叠加模板的装饰层(如金折角+底条)。
- 方案 B(白底换色):把"金底+白字"页翻成"白底+深字"(仅改背景矩形填充色与文字运行色,不改形状/文字)。
一键脚本见 scripts/replace_bg.py(自包含,不依赖外部 unpack/repack 路径)。
前置依赖
- Python(managed venv):
C:/Users/winston/.workbuddy/binaries/python/envs/default/Scripts/python.exe - 包:
lxml、Pillow(可选,仅做像素分析时需要)、python-pptx(可选) - 若用内置
scripts/office/unpack.py:pip install defusedxml(unpack 依赖它)
★ 最关键的一条:所谓"背景图"可能是透明 PNG
不要凭文件名或视觉印象判断背景。 经验(已验证):
- 模板那张"背景图"(如 image1.png)实测是 RGBA 且约 94% 透明——只有金折角、底部条等品牌元素不透明,"黑/白底"区域 alpha=0 是透明的。
- 模板真正的底色来自 主题色 theme1.xml 的 lt1(=window=FFFFFF) 或某张满铺的 solidFill 矩形,不是那张图片。
- 因此模板本质是"白底 + 透明金饰 PNG + logo",不是黑底。若直接把这张图当满铺不透明背景用,透明区会露出下层颜色,效果崩坏。
判定方法(不渲染,本机无 LibreOffice、模型禁用图片预览):
from PIL import Image
import numpy as np
im = Image.open("image1.png").convert("RGBA")
a = np.asarray(im); H,W,_ = a.shape
alpha = a[:,:,3]
print("透明像素占比:", round((alpha<255).sum()/(H*W)*100,1), "%")
print("中心像素 alpha:", int(alpha[H//2,W//2]))
- 透明占比高 → 这是"装饰层",应叠加而非"铺底"。
- 若确需满铺底色,去
theme1.xml或满铺矩形的solidFill找真正颜色。
标准流程(方案 A + 可选 B)
1. 复制两份,绝不碰原文件
cp 目标.pptx work/target.pptx
cp 模板.pptx work/template.pptx
2. 分析模板背景结构
解开模板,看 ppt/slideMasters、ppt/slideLayouts、ppt/slides、ppt/media:
- 背景常定义在 slideLayout / master,形式是"透明装饰 PNG(<p:pic> 满铺)+ 主题底色"。
- 用上节的像素分析法确认装饰图的透明度。
3. 分析目标每页:分两类
遍历 ppt/slides/slideN.xml,按"满铺矩形填充色 + 文字运行色"把页分成:
- 内容页:白底(FFFFFF)+ 深字(如 1F1F1F)→ 直接叠装饰层即可。
- 金底页:满铺 C57F0A(可能叠半透明 7A4A00)+ 浅色/白字(FFFFFF / E8C88A / FBE7C4 / F0DCB4 / FEF6E9)。
# 诊断脚本(打印每页 fills 与文字色)
from lxml import etree
A="{http://schemas.openxmlformats.org/drawingml/2006/main}"
for i in range(1,17):
r=etree.parse(f"target_unpacked/ppt/slides/slide{i}.xml").getroot()
fills=[c.get("val").upper() for c in r.iter(A+"srgbClr")
if etree.QName(c.getparent()).localname=="solidFill"]
txt=set()
for rpr in r.iter(A+"rPr"):
for c in rpr.iter(A+"srgbClr"): txt.add(c.get("val").upper())
print(i, fills, sorted(txt))
4. 一键执行(推荐)
python replace_bg.py build \
--target 目标.pptx --template 模板.pptx --out 结果.pptx [--white]
- 不带
--white:方案 A(叠加装饰,底色/文字全不动)。 - 带
--white:方案 B(金底页翻白底 + 白字改深字,仅改背景矩形填色与文字色)。 - 也可分步:
extract抽装饰图 →overlay仅叠加 →white仅翻白底。
5. 校验(必须做,证明"文字零改动")
from lxml import etree
import zipfile, re
A="{http://schemas.openxmlformats.org/drawingml/2006/main}"
def texts(p):
z=zipfile.ZipFile(p); out={}
for n in sorted(z.namelist()):
if re.match(r"ppt/slides/slide\d+\.xml$",n):
r=etree.fromstring(z.read(n)); out[n]=[t.text or "" for t in r.iter(A+"t")]
return out
o=texts("目标.pptx"); n=texts("结果.pptx")
diffs=sum(1 for k in o if o[k]!=n.get(k))
print("差异页:", diffs, " 字符数一致:",
sum(len("".join(v)) for v in o.values())==sum(len("".join(v)) for v in n.values()))
- 期望:
差异页=0,字符数一致。 - 另查:zip
testzip()==None;[Content_Types].xml为首个条目;所有页都引用了装饰图 rId。 - 方案 B 另查:金底页无残留浅色文字(白底上看不见)。
OOXML / lxml 操作要点
- z-order:
spTree子元素顺序即层级,越靠前越底层。背景矩形通常在最前(底部),内容在后(顶部)。 - 插入装饰层:找到第一个满铺
<p:sp>(背景矩形),在其后spTree.insert(idx+1, pic)—— 这样装饰在背景之上、内容之下。 - 加图三件套:① 把 PNG 放进
ppt/media/;② 在该页_rels/slideN.xml.rels加
<Relationship Id="rIdNNN" Type=".../image" Target="../media/xxx.png"/>;
③ 在 slide XML 加<p:pic>用r:embed="rIdNNN"引用(满铺 xfrm:0,0 → 12192000×6858000 EMU)。 - rId 分配:每页取
max(existing rId)+1,避免冲突。 - 写回 XML:
tree.write(path, xml_declaration=True, encoding="UTF-8", standalone=True)。
已踩坑(必看,否则会漏改/报错)
- grpSp 内层文本:OOXML 里
grpSp(组图形)的子形状是直接子节点,没有spTree包裹。
→ 递归遍历必须for el in grpSp:直接扫其子节点,不能grpSp.find(spTree)(会返回 None,漏掉整组文字)。 - 金底检测时机:要在"底色被改白之前"、且每次从原始 target 重新解包起步。一旦先跑过白底转换,C57F0A 已变白,再检测就漏判。
- XPath 前缀:必须用短前缀
p:a:r:,不能把完整 URI 当前缀(el.find(".//"+A+":xfrm")错;用el.find(f".//{A}xfrm"))。 - 重打包绝不写删除:
repack用zipfile直接以'w'模式覆盖写出即可,不要os.remove/rmtree旧文件(沙箱会判为批量删除而拦截)。 - Content_Types:PNG 的
<Default Extension="png" .../>目标原本就有;脚本里仍建议补一道"不存在则加"。 - 本机无 LibreOffice、模型禁用图片预览 → 用 Pillow 像素分析 + 文本 diff 做 QA,不靠肉眼/渲染。
- 白底上白字会"消失":方案 B 必须把浅色/白字重映射为深色(如 FFFFFF→1F1F1F、E8C88A→C57F0A、FBE7C4/F0DCB4/FEF6E9→7A4A00),且只改
rPr/endParaRPr内的文字色,不要动形状填充(除那两张背景矩形)。
与内置 pptx 技能的关系
本技能是内置 pptx 技能在"背景级编辑"场景下的专用补充:内置技能负责读/建/改内容,本技能负责在不改内容的前提下替换/叠加背景视觉。两者配合:先用本技能换背景,再用内置 pptx 技能微调文字版式。
复用脚本 scripts/replace_bg.py 接口
extract TEMPLATE.pptx deco.png—— 从模板抽出满铺装饰图(自带透明度)。build --target T --template TPL --out O [--white]—— 端到端:抽装饰 → 解包 → 每页叠加 →(可选)金底翻白底 → 重打包 → 文本校验。- 库调用:
from replace_bg import extract_deco_from_template, overlay_deco, convert_gold_to_white, verify_text_identical, unzip, pack_dir - 颜色映射/阈值在脚本顶部常量区,按需调整(不同模板的金/浅色代码不同)。