利用Python自动化处理PPT样式与结构:从提取到生成

发布于:2025-04-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

引言

PowerPoint(PPT)是职场中常用的办公工具,但手动设计和调整样式往往耗时耗力。本文将介绍一套基于Python的自动化解决方案,通过代码实现以下功能:

  1. 提取PPT样式:将PPT的文本格式、颜色、布局等信息保存为JSON文件。
  2. 应用样式到模板:根据JSON定义的样式生成新PPT。
  3. 幻灯片增删与复制:灵活调整PPT结构,满足动态内容需求。

代码功能概述

核心功能模块

  1. 样式提取与保存

    • 函数:extract_ppt_with_style(ppt_path, output_json)
    • 功能:遍历PPT的每一页,提取文本框的字体、颜色、段落对齐方式等样式信息,并保存为结构化的JSON文件。
  2. 样式应用与生成

    • 函数:apply_styles_to_ppt(template_path, json_path, output_pptx, data_json_llm)
    • 功能:根据JSON定义的样式,将文本内容和格式应用到指定模板,生成符合要求的新PPT。
  3. 幻灯片结构管理

    • 函数:copy_slide_and_insert_afterdelete_slidecopy_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(尾)

应用场景

  1. 企业报告自动化:根据数据动态生成季度报告,保持统一格式。
  2. 培训材料生成:批量创建多套PPT,仅需调整中间内容页。
  3. 营销素材管理:快速复制产品介绍模板,替换文本和样式。

总结

本文提供的代码库实现了从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)