471 lines
17 KiB
Python
471 lines
17 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
fast_pptx_renderer.py — 极速 PPTX 渲染器
|
|||
|
|
接收 DeckContract JSON,生成专业 PPTX
|
|||
|
|
|
|||
|
|
使用方法:
|
|||
|
|
python fast_pptx_renderer.py --deck-json <path> --output <path> --theme <theme>
|
|||
|
|
|
|||
|
|
主题:
|
|||
|
|
deepblue - 深海蓝(默认)
|
|||
|
|
caramel - 焦糖色
|
|||
|
|
chinese - 中国红
|
|||
|
|
|
|||
|
|
输出:
|
|||
|
|
JSON 格式: { "ok": true, "path": "...", "size": 12345, "slides": 7 }
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from pptx import Presentation
|
|||
|
|
from pptx.dml.color import RGBColor
|
|||
|
|
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
|
|||
|
|
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
|
|||
|
|
from pptx.util import Inches, Pt
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── 主题配色 ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
THEMES = {
|
|||
|
|
"deepblue": {
|
|||
|
|
"bg": RGBColor(10, 28, 50),
|
|||
|
|
"bg_gradient_end": RGBColor(15, 40, 70),
|
|||
|
|
"panel": RGBColor(255, 255, 255),
|
|||
|
|
"primary": RGBColor(255, 255, 255),
|
|||
|
|
"text_dark": RGBColor(30, 30, 40),
|
|||
|
|
"accent": RGBColor(24, 137, 141),
|
|||
|
|
"accent_light": RGBColor(200, 230, 230),
|
|||
|
|
"gold": RGBColor(201, 160, 39),
|
|||
|
|
"muted": RGBColor(150, 160, 175),
|
|||
|
|
"line": RGBColor(220, 225, 235),
|
|||
|
|
"good": RGBColor(24, 168, 100),
|
|||
|
|
"alert": RGBColor(200, 60, 60),
|
|||
|
|
},
|
|||
|
|
"caramel": {
|
|||
|
|
"bg": RGBColor(248, 245, 239),
|
|||
|
|
"bg_gradient_end": RGBColor(240, 235, 225),
|
|||
|
|
"panel": RGBColor(255, 255, 255),
|
|||
|
|
"primary": RGBColor(38, 38, 38),
|
|||
|
|
"text_dark": RGBColor(38, 38, 38),
|
|||
|
|
"accent": RGBColor(206, 138, 44),
|
|||
|
|
"accent_light": RGBColor(240, 230, 208),
|
|||
|
|
"gold": RGBColor(206, 138, 44),
|
|||
|
|
"muted": RGBColor(96, 96, 96),
|
|||
|
|
"line": RGBColor(223, 214, 195),
|
|||
|
|
"good": RGBColor(24, 108, 79),
|
|||
|
|
"alert": RGBColor(173, 68, 52),
|
|||
|
|
},
|
|||
|
|
"chinese": {
|
|||
|
|
"bg": RGBColor(180, 30, 30),
|
|||
|
|
"bg_gradient_end": RGBColor(140, 20, 20),
|
|||
|
|
"panel": RGBColor(255, 255, 255),
|
|||
|
|
"primary": RGBColor(255, 255, 255),
|
|||
|
|
"text_dark": RGBColor(40, 40, 40),
|
|||
|
|
"accent": RGBColor(220, 180, 60),
|
|||
|
|
"accent_light": RGBColor(255, 240, 200),
|
|||
|
|
"gold": RGBColor(220, 180, 60),
|
|||
|
|
"muted": RGBColor(180, 160, 160),
|
|||
|
|
"line": RGBColor(200, 180, 180),
|
|||
|
|
"good": RGBColor(100, 180, 100),
|
|||
|
|
"alert": RGBColor(255, 200, 100),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
FONT_CN = "Microsoft YaHei"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── 工具函数 ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def money(value: float | int) -> str:
|
|||
|
|
"""格式化金额。"""
|
|||
|
|
return f"{round(float(value)):,}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pct(value: float) -> str:
|
|||
|
|
"""格式化百分比。"""
|
|||
|
|
return f"{value:.2f}%"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_bg(slide, colors: dict):
|
|||
|
|
"""设置幻灯片背景色。"""
|
|||
|
|
fill = slide.background.fill
|
|||
|
|
fill.solid()
|
|||
|
|
fill.fore_color.rgb = colors["bg"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_textbox(slide, left, top, width, height, text, size=18, bold=False,
|
|||
|
|
color=None, align=PP_ALIGN.LEFT, line_spacing=1.5, colors=None):
|
|||
|
|
"""添加文本框。"""
|
|||
|
|
if color is None:
|
|||
|
|
color = colors["primary"] if colors else RGBColor(255, 255, 255)
|
|||
|
|
box = slide.shapes.add_textbox(left, top, width, height)
|
|||
|
|
tf = box.text_frame
|
|||
|
|
tf.clear()
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
tf.vertical_anchor = MSO_ANCHOR.TOP
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.alignment = align
|
|||
|
|
p.line_spacing = line_spacing
|
|||
|
|
run = p.add_run()
|
|||
|
|
run.text = str(text)
|
|||
|
|
run.font.name = FONT_CN
|
|||
|
|
run.font.size = Pt(size)
|
|||
|
|
run.font.bold = bold
|
|||
|
|
run.font.color.rgb = color
|
|||
|
|
return box
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_paragraphs(slide, left, top, width, height, lines, size=15, color=None,
|
|||
|
|
bullet=False, line_spacing=1.5, colors=None):
|
|||
|
|
"""添加多行文本。"""
|
|||
|
|
if color is None:
|
|||
|
|
color = colors["primary"] if colors else RGBColor(255, 255, 255)
|
|||
|
|
box = slide.shapes.add_textbox(left, top, width, height)
|
|||
|
|
tf = box.text_frame
|
|||
|
|
tf.clear()
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
tf.vertical_anchor = MSO_ANCHOR.TOP
|
|||
|
|
for idx, line in enumerate(lines):
|
|||
|
|
p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
|
|||
|
|
p.alignment = PP_ALIGN.LEFT
|
|||
|
|
p.line_spacing = line_spacing
|
|||
|
|
p.text = f"• {line}" if bullet else str(line)
|
|||
|
|
if p.runs:
|
|||
|
|
run = p.runs[0]
|
|||
|
|
run.font.name = FONT_CN
|
|||
|
|
run.font.size = Pt(size)
|
|||
|
|
run.font.color.rgb = color
|
|||
|
|
return box
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_card(slide, left, top, width, height, title, value, subtitle="",
|
|||
|
|
accent=False, colors=None, title_size=12, value_size=20, subtitle_size=10.5):
|
|||
|
|
"""添加卡片。"""
|
|||
|
|
if colors is None:
|
|||
|
|
colors = THEMES["deepblue"]
|
|||
|
|
shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left, top, width, height)
|
|||
|
|
shape.fill.solid()
|
|||
|
|
shape.fill.fore_color.rgb = colors["accent_light"] if accent else colors["panel"]
|
|||
|
|
shape.line.color.rgb = colors["line"]
|
|||
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.12), width - Inches(0.28),
|
|||
|
|
Inches(0.22), title, size=title_size, bold=True, color=colors["muted"],
|
|||
|
|
line_spacing=1.2, colors=colors)
|
|||
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.38), width - Inches(0.28),
|
|||
|
|
Inches(0.4), value, size=value_size, bold=True,
|
|||
|
|
color=colors["accent"] if accent else colors["text_dark"],
|
|||
|
|
line_spacing=1.2, colors=colors)
|
|||
|
|
if subtitle:
|
|||
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.82), width - Inches(0.28),
|
|||
|
|
Inches(0.22), subtitle, size=subtitle_size, color=colors["muted"],
|
|||
|
|
line_spacing=1.2, colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_footer(slide, text, colors=None):
|
|||
|
|
"""添加页脚。"""
|
|||
|
|
if colors is None:
|
|||
|
|
colors = THEMES["deepblue"]
|
|||
|
|
add_textbox(slide, Inches(0.65), Inches(7.0), Inches(12.0), Inches(0.2),
|
|||
|
|
text, size=10, color=colors["muted"], line_spacing=1.2, colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── 幻灯片生成 ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def add_cover_slide(prs: Presentation, product: dict, customer: dict, company: dict, colors: dict):
|
|||
|
|
"""封面页。"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6]) # 空白布局
|
|||
|
|
add_bg(slide, colors)
|
|||
|
|
|
|||
|
|
product_name = product.get("productName", "保险计划书")
|
|||
|
|
customer_name = customer.get("name", "客户")
|
|||
|
|
company_name = company.get("displayName", "")
|
|||
|
|
|
|||
|
|
# 客户姓名
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(1.5), Inches(11.7), Inches(1.0),
|
|||
|
|
customer_name, font_size=42, bold=True, colors=colors)
|
|||
|
|
|
|||
|
|
# 副标题
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(2.5), Inches(11.7), Inches(0.6),
|
|||
|
|
"财富增值与传承方案", size=24, color=colors["muted"], colors=colors)
|
|||
|
|
|
|||
|
|
# 产品名称
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(3.5), Inches(11.7), Inches(0.5),
|
|||
|
|
product_name, size=16, color=colors["muted"], colors=colors)
|
|||
|
|
|
|||
|
|
# 公司名称
|
|||
|
|
if company_name:
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(4.2), Inches(11.7), Inches(0.4),
|
|||
|
|
company_name, size=14, color=colors["accent"], colors=colors)
|
|||
|
|
|
|||
|
|
# 关键指标卡片
|
|||
|
|
policy = product.get("policy", {})
|
|||
|
|
annual_premium = policy.get("annualPremium", 0)
|
|||
|
|
pay_years = policy.get("payYears", 0)
|
|||
|
|
benefit_rows = product.get("benefitRows", [])
|
|||
|
|
|
|||
|
|
total_premium = annual_premium * pay_years
|
|||
|
|
multiple = "-"
|
|||
|
|
if benefit_rows:
|
|||
|
|
last_row = benefit_rows[-1]
|
|||
|
|
final_value = last_row.get("totalSurrenderValue", 0)
|
|||
|
|
if total_premium > 0:
|
|||
|
|
multiple = f"{final_value / total_premium:.1f}x"
|
|||
|
|
|
|||
|
|
metrics = [
|
|||
|
|
("年缴保费", f"${money(annual_premium)}"),
|
|||
|
|
("缴费年期", f"{pay_years}年"),
|
|||
|
|
("总投入", f"${money(total_premium)}"),
|
|||
|
|
("期末倍数", multiple),
|
|||
|
|
]
|
|||
|
|
for i, (label, value) in enumerate(metrics):
|
|||
|
|
x = Inches(0.8 + i * 3.0)
|
|||
|
|
add_card(slide, x, Inches(5.0), Inches(2.5), Inches(1.2),
|
|||
|
|
label, value, colors=colors, accent=(i == 3))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_overview_slide(prs: Presentation, product: dict, company: dict, colors: dict):
|
|||
|
|
"""数据总览页。"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
add_bg(slide, colors)
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_textbox(slide, Inches(0.5), Inches(0.3), Inches(12.3), Inches(0.6),
|
|||
|
|
"保单数据总览", size=28, bold=True, colors=colors)
|
|||
|
|
|
|||
|
|
# 基本信息
|
|||
|
|
insured = product.get("insured", {})
|
|||
|
|
policy = product.get("policy", {})
|
|||
|
|
|
|||
|
|
info_items = [
|
|||
|
|
("产品名称", product.get("productName", "")),
|
|||
|
|
("受保人", f"{insured.get('name', '')} ({insured.get('age', '')}岁)"),
|
|||
|
|
("保单货币", policy.get("currency", "USD")),
|
|||
|
|
("年缴保费", f"${money(policy.get('annualPremium', 0))}"),
|
|||
|
|
("缴费年期", f"{policy.get('payYears', 0)}年"),
|
|||
|
|
("保障期间", policy.get("coveragePeriod", "")),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for i, (label, value) in enumerate(info_items):
|
|||
|
|
y = Inches(1.2 + i * 0.6)
|
|||
|
|
# 标签
|
|||
|
|
add_textbox(slide, Inches(1.0), y, Inches(3.0), Inches(0.4),
|
|||
|
|
label, size=14, color=colors["muted"], colors=colors)
|
|||
|
|
# 值
|
|||
|
|
add_textbox(slide, Inches(4.0), y, Inches(8.0), Inches(0.4),
|
|||
|
|
str(value), size=14, bold=True, color=colors["text_dark"], colors=colors)
|
|||
|
|
|
|||
|
|
# 销售洞察
|
|||
|
|
sales_insights = product.get("salesInsights", {})
|
|||
|
|
if sales_insights:
|
|||
|
|
add_textbox(slide, Inches(0.5), Inches(5.0), Inches(12.3), Inches(0.5),
|
|||
|
|
"销售洞察", size=20, bold=True, colors=colors)
|
|||
|
|
|
|||
|
|
key_points = sales_insights.get("keySellingPoints", [])
|
|||
|
|
if key_points:
|
|||
|
|
add_paragraphs(slide, Inches(1.0), Inches(5.5), Inches(11.0), Inches(1.5),
|
|||
|
|
key_points[:4], size=13, bullet=True, colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_benefit_table_slide(prs: Presentation, product: dict, colors: dict):
|
|||
|
|
"""利益演示页。"""
|
|||
|
|
benefit_rows = product.get("benefitRows", [])
|
|||
|
|
if not benefit_rows:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
add_bg(slide, colors)
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_textbox(slide, Inches(0.5), Inches(0.3), Inches(12.3), Inches(0.6),
|
|||
|
|
"逐年利益演示", size=28, bold=True, colors=colors)
|
|||
|
|
|
|||
|
|
# 表头
|
|||
|
|
headers = ["保单年度", "年龄", "已缴保费", "保证现金价值", "归原红利", "终期分红", "退保发还总额"]
|
|||
|
|
col_widths = [1.5, 1.2, 1.8, 2.0, 1.8, 1.8, 2.2]
|
|||
|
|
col_x = [0.5]
|
|||
|
|
for w in col_widths[:-1]:
|
|||
|
|
col_x.append(col_x[-1] + w)
|
|||
|
|
|
|||
|
|
for header, x in zip(headers, col_x):
|
|||
|
|
add_textbox(slide, Inches(x), Inches(1.1), Inches(col_widths[headers.index(header)]),
|
|||
|
|
Inches(0.4), header, size=11, bold=True, color=colors["accent"], colors=colors)
|
|||
|
|
|
|||
|
|
# 数据行(最多20行)
|
|||
|
|
display_rows = benefit_rows[:20]
|
|||
|
|
for row_idx, row in enumerate(display_rows):
|
|||
|
|
y = Inches(1.5 + row_idx * 0.35)
|
|||
|
|
values = [
|
|||
|
|
str(row.get("policyYear", "")),
|
|||
|
|
str(row.get("age", "")),
|
|||
|
|
f"${money(row.get('totalPremiumPaid', 0))}",
|
|||
|
|
f"${money(row.get('guaranteedCashValue', 0))}",
|
|||
|
|
f"${money(row.get('reversionaryBonus', 0))}",
|
|||
|
|
f"${money(row.get('terminalDividend', 0))}",
|
|||
|
|
f"${money(row.get('totalSurrenderValue', 0))}",
|
|||
|
|
]
|
|||
|
|
for value, x in zip(values, col_x):
|
|||
|
|
add_textbox(slide, Inches(x), y, Inches(1.5), Inches(0.3),
|
|||
|
|
value, size=10, color=colors["text_dark"], colors=colors)
|
|||
|
|
|
|||
|
|
add_footer(slide, f"共 {len(benefit_rows)} 年数据,显示前 20 年", colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_withdrawal_slide(prs: Presentation, product: dict, colors: dict):
|
|||
|
|
"""退保演示页。"""
|
|||
|
|
withdrawal_rows = product.get("withdrawalRows", [])
|
|||
|
|
if not withdrawal_rows:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
add_bg(slide, colors)
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_textbox(slide, Inches(0.5), Inches(0.3), Inches(12.3), Inches(0.6),
|
|||
|
|
"退保/提取演示", size=28, bold=True, colors=colors)
|
|||
|
|
|
|||
|
|
# 表头
|
|||
|
|
headers = ["保单年度", "年龄", "当年提取", "累计提取", "提取后退保价值"]
|
|||
|
|
col_widths = [1.8, 1.5, 2.0, 2.0, 2.5]
|
|||
|
|
col_x = [0.8]
|
|||
|
|
for w in col_widths[:-1]:
|
|||
|
|
col_x.append(col_x[-1] + w)
|
|||
|
|
|
|||
|
|
for header, x in zip(headers, col_x):
|
|||
|
|
add_textbox(slide, Inches(x), Inches(1.1), Inches(col_widths[headers.index(header)]),
|
|||
|
|
Inches(0.4), header, size=12, bold=True, color=colors["accent"], colors=colors)
|
|||
|
|
|
|||
|
|
# 数据行
|
|||
|
|
display_rows = withdrawal_rows[:15]
|
|||
|
|
for row_idx, row in enumerate(display_rows):
|
|||
|
|
y = Inches(1.6 + row_idx * 0.38)
|
|||
|
|
values = [
|
|||
|
|
str(row.get("policyYear", "")),
|
|||
|
|
str(row.get("age", "")),
|
|||
|
|
f"${money(row.get('annualWithdrawal', 0))}",
|
|||
|
|
f"${money(row.get('cumulativeWithdrawal', 0))}",
|
|||
|
|
f"${money(row.get('surrenderValueAfter', 0))}",
|
|||
|
|
]
|
|||
|
|
for value, x in zip(values, col_x):
|
|||
|
|
add_textbox(slide, Inches(x), y, Inches(2.0), Inches(0.3),
|
|||
|
|
value, size=11, color=colors["text_dark"], colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_summary_slide(prs: Presentation, product: dict, colors: dict):
|
|||
|
|
"""总结页。"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
add_bg(slide, colors)
|
|||
|
|
|
|||
|
|
policy = product.get("policy", {})
|
|||
|
|
benefit_rows = product.get("benefitRows", [])
|
|||
|
|
|
|||
|
|
annual_premium = policy.get("annualPremium", 0)
|
|||
|
|
pay_years = policy.get("payYears", 0)
|
|||
|
|
total_premium = annual_premium * pay_years
|
|||
|
|
|
|||
|
|
final_value = 0
|
|||
|
|
multiple = "-"
|
|||
|
|
if benefit_rows:
|
|||
|
|
last_row = benefit_rows[-1]
|
|||
|
|
final_value = last_row.get("totalSurrenderValue", 0)
|
|||
|
|
if total_premium > 0:
|
|||
|
|
multiple = f"{final_value / total_premium:.1f}x"
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(2.0), Inches(11.7), Inches(1.0),
|
|||
|
|
"方案总结", size=36, bold=True, align=PP_ALIGN.CENTER, colors=colors)
|
|||
|
|
|
|||
|
|
# 核心数据
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(3.2), Inches(11.7), Inches(0.6),
|
|||
|
|
f"总投入 ${money(total_premium)} → 期末价值 ${money(final_value)}",
|
|||
|
|
size=24, color=colors["muted"], align=PP_ALIGN.CENTER, colors=colors)
|
|||
|
|
|
|||
|
|
add_textbox(slide, Inches(0.8), Inches(4.0), Inches(11.7), Inches(0.5),
|
|||
|
|
f"倍数增长 {multiple}", size=28, bold=True, color=colors["gold"],
|
|||
|
|
align=PP_ALIGN.CENTER, colors=colors)
|
|||
|
|
|
|||
|
|
# 销售洞察
|
|||
|
|
sales_insights = product.get("salesInsights", {})
|
|||
|
|
if sales_insights:
|
|||
|
|
narrative = sales_insights.get("suggestedNarrative", "")
|
|||
|
|
if narrative:
|
|||
|
|
add_textbox(slide, Inches(1.5), Inches(5.0), Inches(10.3), Inches(1.0),
|
|||
|
|
narrative, size=16, color=colors["muted"],
|
|||
|
|
align=PP_ALIGN.CENTER, colors=colors)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── 主渲染函数 ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def render_deck(deck: dict, output_path: str, theme: str = "deepblue") -> dict:
|
|||
|
|
"""渲染 DeckContract 为 PPTX。"""
|
|||
|
|
colors = THEMES.get(theme, THEMES["deepblue"])
|
|||
|
|
|
|||
|
|
prs = Presentation()
|
|||
|
|
prs.slide_width = Inches(13.33)
|
|||
|
|
prs.slide_height = Inches(7.5)
|
|||
|
|
|
|||
|
|
products = deck.get("products", [])
|
|||
|
|
customer = deck.get("customer", {})
|
|||
|
|
company = deck.get("company", {})
|
|||
|
|
|
|||
|
|
for product in products:
|
|||
|
|
# 封面页
|
|||
|
|
add_cover_slide(prs, product, customer, company, colors)
|
|||
|
|
# 数据总览
|
|||
|
|
add_overview_slide(prs, product, company, colors)
|
|||
|
|
# 利益演示
|
|||
|
|
add_benefit_table_slide(prs, product, colors)
|
|||
|
|
# 退保演示(如果有)
|
|||
|
|
add_withdrawal_slide(prs, product, colors)
|
|||
|
|
# 总结页
|
|||
|
|
add_summary_slide(prs, product, colors)
|
|||
|
|
|
|||
|
|
# 保存
|
|||
|
|
prs.save(output_path)
|
|||
|
|
|
|||
|
|
# 统计
|
|||
|
|
file_size = os.path.getsize(output_path)
|
|||
|
|
slide_count = len(prs.slides)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"ok": True,
|
|||
|
|
"path": output_path,
|
|||
|
|
"size": file_size,
|
|||
|
|
"slides": slide_count,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── CLI 入口 ────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(description="Fast PPTX Renderer")
|
|||
|
|
parser.add_argument("--deck-json", required=True, help="DeckContract JSON 文件路径")
|
|||
|
|
parser.add_argument("--output", required=True, help="输出 PPTX 文件路径")
|
|||
|
|
parser.add_argument("--theme", default="deepblue", choices=["deepblue", "caramel", "chinese"],
|
|||
|
|
help="主题配色")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 读取 DeckContract JSON
|
|||
|
|
with open(args.deck_json, "r", encoding="utf-8") as f:
|
|||
|
|
deck = json.load(f)
|
|||
|
|
|
|||
|
|
# 渲染
|
|||
|
|
result = render_deck(deck, args.output, args.theme)
|
|||
|
|
|
|||
|
|
# 输出结果到 stdout
|
|||
|
|
print(json.dumps(result, ensure_ascii=False))
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|