引言
PowerPoint(PPT)是职场中常用的办公工具,但手动设计和调整样式往往耗时耗力。本文将介绍一套基于Python的自动化解决方案,通过代码实现以下功能:
- 提取PPT样式:将PPT的文本格式、颜色、布局等信息保存为JSON文件。
- 应用样式到模板:根据JSON定义的样式生成新PPT。
- 幻灯片增删与复制:灵活调整PPT结构,满足动态内容需求。
代码功能概述
核心功能模块
样式提取与保存
- 函数:
extract_ppt_with_style(ppt_path, output_json)
- 功能:遍历PPT的每一页,提取文本框的字体、颜色、段落对齐方式等样式信息,并保存为结构化的JSON文件。
- 函数:
样式应用与生成
- 函数:
apply_styles_to_ppt(template_path, json_path, output_pptx, data_json_llm)
- 功能:根据JSON定义的样式,将文本内容和格式应用到指定模板,生成符合要求的新PPT。
- 函数:
幻灯片结构管理
- 函数:
copy_slide_and_insert_after
、delete_slide
、copy_ppt
- 功能:复制、删除幻灯片,并根据需求动态调整中间页的数量(如扩展或压缩内容页)。
- 函数:
代码详解
1. 提取PPT样式(extract_ppt_with_style
)
def extract_ppt_with_style(ppt_path, output_json):
prs = Presentation(ppt_path)
data = []
for slide_idx, slide in enumerate(prs.slides):
slide_data = {
"slide_number": slide_idx + 1,
"shapes": []
}
for shape in slide.shapes:
if not shape.has_text_frame:
continue # 跳过非文本形状
text_frame = shape.text_frame
text_info = {
"shape_name": shape.name,
"paragraphs": []
}
for paragraph in text_frame.paragraphs:
para_info = {
"alignment": str(paragraph.alignment),
"runs": []
}
for run in paragraph.runs:
run_info = {
"text": run.text,
"font": {
"name": run.font.name,
"size": str(run.font.size),
"bold": run.font.bold,
"italic": run.font.italic,
"color": {
"type": "theme" if run.font.color.type == MSO_THEME_COLOR else "rgb",
"theme_color": run.font.color.theme_color,
"rgb": (run.font.color.rgb[0], run.font.color.rgb[1], run.font.color.rgb[2]) if run.font.color.rgb else None
}
}
}
para_info["runs"].append(run_info)
text_info["paragraphs"].append(para_info)
slide_data["shapes"].append(text_info)
data.append(slide_data)
# 保存并压缩JSON
with open(output_json, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
data = json_compress(data)
with open("compress_" + output_json, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return data
- 关键点:
- 样式解析:记录字体名称、大小、粗体、斜体、颜色(主题色或RGB值)。
- 结构化存储:JSON中每一页(
slide
)包含多个形状(shapes
),每个形状包含段落(paragraphs
)和文本片段(runs
)。 - 压缩优化:
json_compress
函数简化冗余数据(如通用形状名称),提升存储效率。
2. 应用样式生成PPT(apply_styles_to_ppt
)
def apply_styles_to_ppt(template_path, json_path, output_pptx, data_json_llm):
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
prs = Presentation(template_path)
for slide_idx, slide in enumerate(prs.slides):
for shape_idx, shape in enumerate(slide.shapes):
if not shape.has_text_frame:
continue
text_frame = shape.text_frame
for paragraph_idx, paragraph in enumerate(text_frame.paragraphs):
for run_idx, run in enumerate(paragraph.runs):
run_info = data[slide_idx]["shapes"][shape_idx]["paragraphs"][paragraph_idx]["runs"][run_idx]
# 应用文本内容
run.text = data_json_llm[slide_idx]["shapes"].pop()["paragraphs"]
# 应用样式
run.font.name = run_info["font"]["name"]
run.font.bold = run_info["font"]["bold"]
run.font.italic = run_info["font"]["italic"]
# 处理颜色
color_data = run_info["font"]["color"]
if color_data["type"] == "rgb":
r, g, b = color_data["rgb"]
run.font.color.rgb = RGBColor(r, g, b)
elif color_data["type"] == "theme":
theme_color = getattr(MSO_THEME_COLOR, color_data["theme_color"], MSO_THEME_COLOR.ACCENT_1)
run.font.color.theme_color = theme_color
prs.save(output_pptx)
- 关键点:
- 样式复用:从JSON中读取字体、颜色等信息,直接应用到模板PPT的对应位置。
- 动态内容替换:通过
data_json_llm
参数,可结合LLM(如GPT)生成的文本内容动态填充PPT。
3. 幻灯片结构管理
3.1 复制与插入幻灯片
def copy_slide_and_insert_after(prs, source_index, target_index):
source_slide = prs.slides[source_index]
new_slide = prs.slides.add_slide(source_slide.slide_layout)
# 复制形状和关系
for shape in source_slide.shapes:
new_el = deepcopy(shape.element)
new_slide.shapes._spTree.insert_element_before(new_el, 'p:extLst')
# 调整位置
slides = list(prs.slides._sldIdLst)
new_slide_id = slides.pop()
slides.insert(target_index + 1, new_slide_id)
prs.slides._sldIdLst[:] = slides
- 功能:复制指定幻灯片并插入到目标位置后,保持布局和元素一致性。
3.2 删除幻灯片
def delete_slide(prs, slide_index):
if slide_index < 0 or slide_index >= len(prs.slides):
print("无效的幻灯片索引")
return
xml_slides = list(prs.slides._sldIdLst)
slides_id_to_delete = xml_slides[slide_index]
prs.slides._sldIdLst.remove(slides_id_to_delete)
- 功能:通过移除幻灯片ID实现删除,避免直接操作可能导致的格式错误。
3.3 动态扩展/压缩PPT页数
def copy_ppt(pages, template_path="template.pptx", modified_path="modified.pptx"):
prs = Presentation(template_path)
copy_pages = pages - 2 # 排除首尾固定页
center_pages = len(prs.slides) - 2
if copy_pages < center_pages:
# 删除多余页
for _ in range(center_pages - copy_pages):
delete_slide(prs, len(prs.slides)-1)
else:
# 复制中间页
n = (copy_pages // center_pages) * center_pages
for _ in range(n):
for i in range(1, center_pages+1):
copy_slide_and_insert_after(prs, i, i)
prs.save(modified_path)
- 应用场景:根据需求动态调整中间页的数量(如扩展到5页或压缩到3页),保持首尾页固定。
使用示例
场景1:生成符合样式的PPT
# 1. 提取原始PPT的样式
extract_ppt_with_style("template.pptx", "output_styles.json")
# 2. 生成新内容(例如通过LLM)
llm_json = [...] # LLM生成的文本内容
# 3. 应用样式生成最终PPT
apply_styles_to_ppt("template.pptx", "output_styles.json", "new_ppt.pptx", llm_json)
场景2:动态调整PPT页数
# 假设原始模板有5页(首尾固定,中间3页)
copy_ppt(pages=7, template_path="template.pptx") # 最终生成7页:1(首)+5(中间复制)+1(尾)
应用场景
- 企业报告自动化:根据数据动态生成季度报告,保持统一格式。
- 培训材料生成:批量创建多套PPT,仅需调整中间内容页。
- 营销素材管理:快速复制产品介绍模板,替换文本和样式。
总结
本文提供的代码库实现了从PPT样式提取、动态内容生成到结构管理的全流程自动化。开发者可通过以下方式进一步优化:
- 集成LLM:将文本生成部分与GPT等模型结合,实现从内容到样式的全自动化。
- 图形处理:扩展对图片、图表样式的解析与应用。
- 用户界面:封装为GUI工具,降低使用门槛。
通过这种方式,企业可大幅减少PPT制作时间,专注于内容创新而非格式调整。
希望这篇博客能帮助读者理解如何通过Python自动化处理PPT,提升工作效率!
from pptx import Presentation
from pptx.enum.dml import MSO_THEME_COLOR
from pptx.dml.color import RGBColor
from copy import deepcopy
import json
def extract_ppt_with_style(ppt_path, output_json):
prs = Presentation(ppt_path)
data = []
for slide_idx, slide in enumerate(prs.slides):
slide_data = {
"slide_number": slide_idx + 1,
"shapes": []
}
for shape in slide.shapes:
if not shape.has_text_frame:
continue # 跳过非文本形状
text_frame = shape.text_frame
text_info = {
"shape_name": shape.name,
"paragraphs": []
}
for paragraph in text_frame.paragraphs:
para_info = {
"alignment": str(paragraph.alignment),
"runs": []
}
for run in paragraph.runs:
run_info = {
"text": run.text,
"font": {
"name": run.font.name,
"size": str(run.font.size) if run.font.size else None,
"bold": run.font.bold,
"italic": run.font.italic,
"color": {
"type": "theme" if run.font.color.type == MSO_THEME_COLOR else "rgb",
"theme_color": run.font.color.theme_color,
"rgb": (run.font.color.rgb[0], run.font.color.rgb[1],
run.font.color.rgb[2]) if run.font.color.rgb else None
}
},
# "highlight_color": str(run.highlight_color) # 修改:从 run 而非 run.font 获取
}
para_info["runs"].append(run_info)
text_info["paragraphs"].append(para_info)
slide_data["shapes"].append(text_info)
data.append(slide_data)
with open(output_json, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
data = json_compress(data)
with open("compress" + "_" + output_json, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return data
def apply_styles_to_ppt(template_path, json_path, output_pptx, data_json_llm):
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
prs = Presentation(template_path)
for slide_idx, slide in enumerate(prs.slides):
for shape_idx, shape in enumerate(slide.shapes):
if not shape.has_text_frame:
continue # 跳过非文本形状
text_frame = shape.text_frame
for paragraph_idx, paragraph in enumerate(text_frame.paragraphs):
for run_idx, run in enumerate(paragraph.runs):
run_info = data[slide_idx]["shapes"][shape_idx]["paragraphs"][paragraph_idx]["runs"][run_idx]
text = data_json_llm[slide_idx]["shapes"].pop()
# run.text = run_info["text"]
run.text = text["paragraphs"]
run.font.name = run_info["font"]["name"]
# run.font.size = run_info["font"]["size"]
run.font.bold = run_info["font"]["bold"]
# run.font.size = run_info["font"]["size"]
run.font.italic = run_info["font"]["italic"]
# 假设 run_data 是从 JSON 中读取的字典
color_data = run_info["font"]["color"]
if color_data["type"] == "rgb":
# 解析 RGB 值
r_str, g_str, b_str = color_data["rgb"]
r = r_str
g = g_str
b = b_str
run.font.color.rgb = RGBColor(r, g, b)
elif color_data["type"] == "hex":
# 解析十六进制颜色
hex_color = color_data["hex"].lstrip("#")
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
run.font.color.rgb = RGBColor(r, g, b)
elif color_data["type"] == "theme":
# 使用主题颜色(如 MSO_THEME_COLOR.ACCENT_1)
theme_color_name = color_data["theme_color"]
theme_color = getattr(MSO_THEME_COLOR, theme_color_name, MSO_THEME_COLOR.ACCENT_1)
run.font.color.theme_color = theme_color
else:
# 默认颜色(黑色)
run.font.color.rgb = RGBColor(0, 0, 0)
prs.save(output_pptx)
def json_compress(json_data):
for slide in json_data:
for shape in slide["shapes"]:
if "Shape" in shape["shape_name"]:
shape["paragraphs"] = {}
else:
for paragraph in shape["paragraphs"]:
for run in paragraph["runs"]:
shape["paragraphs"] = run["text"]
json_data_new = []
for slide in json_data:
shapes = {"shapes": [], 'slide_number': slide['slide_number']}
for shape in slide["shapes"]:
if "Shape" in shape["shape_name"]:
shape["paragraphs"] = {}
else:
shapes["shapes"].append(shape)
json_data_new.append(shapes)
return json_data_new
def copy_slide_and_insert_after(prs, source_index, target_index):
"""
复制源幻灯片并将其插入到目标幻灯片的后面。
:param prs: Presentation 对象
:param source_index: 源幻灯片的索引(从0开始)
:param target_index: 目标幻灯片的索引(新幻灯片将插入到其后面)
"""
# 获取源幻灯片
source_slide = prs.slides[source_index]
# 创建新幻灯片(使用相同的布局)
new_slide_layout = source_slide.slide_layout
new_slide = prs.slides.add_slide(new_slide_layout)
# 复制所有形状(包括文本框、图片、图表等)
for shape in source_slide.shapes:
el = shape.element
new_el = deepcopy(el)
new_slide.shapes._spTree.insert_element_before(new_el, 'p:extLst')
# 复制关系(如超链接、注释等)
for rel in source_slide.part.rels.values():
if "notesSlide" not in rel.reltype: # 排除注释页
# 使用 relate_to 方法而不是 add 方法
new_slide.part.relate_to(
rel._target,
rel.reltype
)
# 调整幻灯片顺序:将新幻灯片移动到目标位置的后面
slides = list(prs.slides._sldIdLst)
new_position = target_index + 1 # 插入到目标幻灯片的后面
# 移除刚添加的新幻灯片(默认在最后)
new_slide_id = slides.pop()
# 插入到正确的位置
slides.insert(new_position, new_slide_id)
prs.slides._sldIdLst[:] = slides
def delete_slide(prs, slide_index):
# prs = Presentation(template_path)
"""
删除给定索引处的幻灯片。
:param prs: Presentation 对象
:param slide_index: 要删除的幻灯片的索引(从0开始)
"""
# 确保索引在范围内
if slide_index < 0 or slide_index >= len(prs.slides):
print("无效的幻灯片索引")
return
# 获取幻灯片ID列表
xml_slides = list(prs.slides._sldIdLst)
# 根据索引找到对应的幻灯片ID并移除
slides_id_to_delete = xml_slides[slide_index]
prs.slides._sldIdLst.remove(slides_id_to_delete)
# 保存修改后的PPT
# prs.save(modified_path)
def copy_ppt(pages, template_path="template.pptx", source_index=1, target_index=1,
modified_path="modified_example.pptx"):
prs = Presentation(template_path)
copy_pages, center_pages = pages - 2, len(prs.slides) - 2
if copy_pages != center_pages:
if copy_pages < center_pages:
start_page_index = center_pages
for _ in range(center_pages - copy_pages):
delete_slide(prs, start_page_index)
start_page_index -= 1
else:
n = (copy_pages // center_pages) * center_pages
m = (copy_pages // center_pages + 1) * center_pages - copy_pages
start_page_index = center_pages
for _ in range(n):
for i in range(1, center_pages + 1):
copy_slide_and_insert_after(prs, i, start_page_index)
start_page_index += 1
if m:
for _ in range(m):
delete_slide(prs, start_page_index)
start_page_index -= 1
prs.save(modified_path)
if __name__ == '__main__':
# 使用示例
# data=extract_ppt_with_style("template.pptx", "output_styles.json")
#
# prompt_text=f"""
# # ppt json 模版
# {data}
# # 模版使用说明
# - 每个 slide 的 shapes 的 结构 (元素个数)是不可变得
# - 每个 slide 的 shapes 里面的字典的key 不可变 值是可以变 的
# - 第一 slide 是不可被复制的 且 必须在第一个位置 但是内容 是可变的 slide_number 也是可变的
# - 最后一个 slide 也是不可复制的 且 必须在最后一个位置 但是内容 是可变的 slide_number 也是可变的
# - 中简的 slide 是可以被复制的 但是顺序不能改变
# - 例如 中间 有 两个 slide 2,3 如果你的ppt 中间需要5个 slide 那么复制 顺序是 2,3,2,3,2 复制后可以改其他slide_number 名字
# # 明白上述模版使用要求之后 请 完成主题为:人工智能改变世界的ppt大纲 并且 使用上述模版 生成对应的json
# """
llm_json =[]
# copy_ppt(len(llm_json))
data = extract_ppt_with_style("modified_example.pptx", "output_styles.json")
apply_styles_to_ppt("modified_example.pptx", "output_styles.json", "new_ppt.pptx", llm_json)