425 lines
15 KiB
Python
425 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
gen_slides.py - parchment design system slide deck generator
|
|
|
|
用法:
|
|
pip install python-pptx --break-system-packages
|
|
python3 gen_slides.py
|
|
|
|
输出:
|
|
output.pptx (16:9 宽屏, parchment 风格)
|
|
|
|
这是一个模板脚本 - 填充自己的内容后直接运行。
|
|
"""
|
|
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Pt
|
|
from pptx.dml.color import RGBColor
|
|
from pptx.enum.shapes import MSO_SHAPE
|
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# Design System Constants
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
# 色板
|
|
PARCHMENT = RGBColor(0xf5, 0xf4, 0xed)
|
|
IVORY = RGBColor(0xfa, 0xf9, 0xf5)
|
|
BRAND = RGBColor(0x1B, 0x36, 0x5D)
|
|
BRAND_DEEP = RGBColor(0x1B, 0x36, 0x5D)
|
|
NEAR_BLACK = RGBColor(0x14, 0x14, 0x13)
|
|
DARK_WARM = RGBColor(0x3d, 0x3d, 0x3a)
|
|
CHARCOAL = RGBColor(0x4d, 0x4c, 0x48)
|
|
OLIVE = RGBColor(0x50, 0x4e, 0x49)
|
|
STONE = RGBColor(0x6b, 0x6a, 0x64)
|
|
BORDER = RGBColor(0xe8, 0xe6, 0xdc)
|
|
WHITE = RGBColor(0xff, 0xff, 0xff)
|
|
|
|
# Fonts. Single serif per page. PPT falls back on the viewer's system.
|
|
# For Japanese best-effort output, set LANG = "ja" before generating.
|
|
LANG = "zh"
|
|
CN_SERIF = "Source Han Serif SC"
|
|
JA_SERIF = "YuMincho" # Windows: Yu Mincho; Linux: Noto Serif CJK JP
|
|
|
|
SERIF = JA_SERIF if LANG == "ja" else CN_SERIF
|
|
SANS = SERIF
|
|
|
|
# 16:9 宽屏
|
|
SLIDE_W = Inches(13.33)
|
|
SLIDE_H = Inches(7.5)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# Helpers
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
def blank_slide(prs, bg_color=PARCHMENT):
|
|
"""创建空白幻灯片,指定背景色"""
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6]) # 6 = Blank
|
|
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
|
|
0, 0, prs.slide_width, prs.slide_height)
|
|
bg.fill.solid()
|
|
bg.fill.fore_color.rgb = bg_color
|
|
bg.line.fill.background()
|
|
bg.shadow.inherit = False
|
|
return slide
|
|
|
|
|
|
def add_text(slide, text, left, top, width, height,
|
|
font=SANS, size=18, bold=False, italic=False,
|
|
color=NEAR_BLACK, align=PP_ALIGN.LEFT,
|
|
vanchor=MSO_ANCHOR.TOP):
|
|
"""加一段文字"""
|
|
tb = slide.shapes.add_textbox(left, top, width, height)
|
|
tf = tb.text_frame
|
|
tf.word_wrap = True
|
|
tf.margin_left = tf.margin_right = 0
|
|
tf.margin_top = tf.margin_bottom = 0
|
|
tf.vertical_anchor = vanchor
|
|
p = tf.paragraphs[0]
|
|
p.alignment = align
|
|
run = p.add_run()
|
|
run.text = text
|
|
run.font.name = font
|
|
run.font.size = Pt(size)
|
|
run.font.bold = bold
|
|
run.font.italic = italic
|
|
run.font.color.rgb = color
|
|
return tb
|
|
|
|
|
|
def add_line(slide, left, top, width, color=BRAND, weight_pt=1):
|
|
"""加水平线"""
|
|
line = slide.shapes.add_connector(1, left, top, left + width, top)
|
|
line.line.color.rgb = color
|
|
line.line.width = Pt(weight_pt)
|
|
return line
|
|
|
|
|
|
def add_card(slide, left, top, width, height,
|
|
fill=IVORY, border=BORDER, border_weight=0.5):
|
|
"""加卡片背景"""
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
left, top, width, height)
|
|
card.fill.solid()
|
|
card.fill.fore_color.rgb = fill
|
|
card.line.color.rgb = border
|
|
card.line.width = Pt(border_weight)
|
|
card.shadow.inherit = False
|
|
return card
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# Slide Templates
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
def cover_slide(prs, title, subtitle, author, date):
|
|
"""封面:大标题 + 副标题 + 作者/日期"""
|
|
s = blank_slide(prs)
|
|
# 大标题(serif 44pt 居中)
|
|
add_text(s, title,
|
|
Inches(1), Inches(2.5), Inches(11.33), Inches(1.5),
|
|
font=SERIF, size=44, color=NEAR_BLACK,
|
|
align=PP_ALIGN.CENTER)
|
|
# 品牌色短线
|
|
add_line(s, Inches(6.17), Inches(4.2), Inches(1), weight_pt=1.5)
|
|
# 副标题
|
|
add_text(s, subtitle,
|
|
Inches(1), Inches(4.5), Inches(11.33), Inches(0.8),
|
|
font=SANS, size=18, color=OLIVE,
|
|
align=PP_ALIGN.CENTER)
|
|
# 作者 + 日期
|
|
add_text(s, f"{author} · {date}",
|
|
Inches(1), Inches(6.5), Inches(11.33), Inches(0.4),
|
|
font=SANS, size=13, color=STONE,
|
|
align=PP_ALIGN.CENTER)
|
|
return s
|
|
|
|
|
|
def toc_slide(prs, items):
|
|
"""目录页:01 章节名 列表"""
|
|
s = blank_slide(prs)
|
|
add_text(s, "目录",
|
|
Inches(1.2), Inches(0.8), Inches(10), Inches(0.8),
|
|
font=SERIF, size=32, color=NEAR_BLACK)
|
|
add_line(s, Inches(1.2), Inches(1.8), Inches(11), weight_pt=1)
|
|
|
|
for i, item in enumerate(items):
|
|
y = Inches(2.4 + i * 0.9)
|
|
add_text(s, f"0{i+1}",
|
|
Inches(1.2), y, Inches(1), Inches(0.6),
|
|
font=SERIF, size=28, color=BRAND)
|
|
add_text(s, item,
|
|
Inches(2.4), y, Inches(9), Inches(0.6),
|
|
font=SERIF, size=22, color=NEAR_BLACK,
|
|
vanchor=MSO_ANCHOR.MIDDLE)
|
|
return s
|
|
|
|
|
|
def chapter_slide(prs, number, title):
|
|
"""章节首页:油墨蓝色背景 + 居中大标题"""
|
|
s = blank_slide(prs, bg_color=BRAND)
|
|
add_text(s, f"0{number}",
|
|
Inches(0.8), Inches(0.5), Inches(2), Inches(0.8),
|
|
font=SERIF, size=26, color=WHITE)
|
|
add_text(s, title,
|
|
Inches(1), Inches(3), Inches(11.33), Inches(1.5),
|
|
font=SERIF, size=56, color=WHITE,
|
|
align=PP_ALIGN.CENTER)
|
|
return s
|
|
|
|
|
|
def content_slide(prs, eyebrow, title, body, page_num=None):
|
|
"""内容页:小标题 + 大标题 + 正文"""
|
|
s = blank_slide(prs)
|
|
# eyebrow
|
|
add_text(s, eyebrow,
|
|
Inches(1.2), Inches(0.6), Inches(10), Inches(0.4),
|
|
font=SANS, size=12, color=STONE)
|
|
# title
|
|
add_text(s, title,
|
|
Inches(1.2), Inches(1.2), Inches(11.33), Inches(1.2),
|
|
font=SERIF, size=32, color=NEAR_BLACK)
|
|
# body
|
|
add_text(s, body,
|
|
Inches(1.2), Inches(3), Inches(11), Inches(3.5),
|
|
font=SANS, size=18, color=DARK_WARM)
|
|
# page number
|
|
if page_num is not None:
|
|
add_text(s, f" - {page_num:02d}",
|
|
Inches(11.5), Inches(6.9), Inches(1.5), Inches(0.3),
|
|
font=SANS, size=11, color=STONE,
|
|
align=PP_ALIGN.RIGHT)
|
|
return s
|
|
|
|
|
|
def metrics_slide(prs, title, metrics):
|
|
"""数据页:标题 + N 张数据卡并排
|
|
metrics: [(value, label), ...]
|
|
"""
|
|
s = blank_slide(prs)
|
|
# 标题
|
|
add_text(s, title,
|
|
Inches(1.2), Inches(0.8), Inches(11), Inches(1),
|
|
font=SERIF, size=28, color=NEAR_BLACK,
|
|
align=PP_ALIGN.CENTER)
|
|
add_line(s, Inches(6.17), Inches(2), Inches(1))
|
|
|
|
# 数据卡
|
|
n = len(metrics)
|
|
card_w = Inches(2.8)
|
|
gap = Inches(0.3)
|
|
total_w = card_w * n + gap * (n - 1)
|
|
start = (SLIDE_W - total_w) / 2
|
|
|
|
for i, (value, label) in enumerate(metrics):
|
|
x = start + (card_w + gap) * i
|
|
# 大数字
|
|
add_text(s, value,
|
|
x, Inches(3), card_w, Inches(1.5),
|
|
font=SERIF, size=52, color=BRAND,
|
|
align=PP_ALIGN.CENTER)
|
|
# 标签
|
|
add_text(s, label,
|
|
x, Inches(4.8), card_w, Inches(0.6),
|
|
font=SANS, size=14, color=OLIVE,
|
|
align=PP_ALIGN.CENTER)
|
|
return s
|
|
|
|
|
|
def quote_slide(prs, quote, source):
|
|
"""引用页:极简,居中引文"""
|
|
s = blank_slide(prs)
|
|
add_text(s, f"\u201c{quote}\u201d",
|
|
Inches(1.5), Inches(2.8), Inches(10.33), Inches(2.5),
|
|
font=SERIF, size=28, color=NEAR_BLACK,
|
|
align=PP_ALIGN.CENTER,
|
|
vanchor=MSO_ANCHOR.MIDDLE)
|
|
add_text(s, f" - {source}",
|
|
Inches(1.5), Inches(5.2), Inches(10.33), Inches(0.4),
|
|
font=SANS, size=14, color=OLIVE,
|
|
align=PP_ALIGN.CENTER)
|
|
return s
|
|
|
|
|
|
def comparison_slide(prs, eyebrow, left_title, left_items, right_title, right_items, page_num=None):
|
|
"""对比页:左右两栏,竖线分隔,左侧降调,右侧全色
|
|
left_items / right_items: list of str (最多 4 条)
|
|
"""
|
|
s = blank_slide(prs)
|
|
add_text(s, eyebrow,
|
|
Inches(1.2), Inches(0.6), Inches(10), Inches(0.4),
|
|
font=SANS, size=12, color=STONE)
|
|
# 分隔竖线(居中)
|
|
divider = s.shapes.add_connector(1,
|
|
Inches(6.67), Inches(1.0),
|
|
Inches(6.67), Inches(6.8))
|
|
divider.line.color.rgb = BORDER
|
|
divider.line.width = Pt(1)
|
|
# 左栏标题(降调)
|
|
add_text(s, left_title,
|
|
Inches(1.2), Inches(1.2), Inches(5), Inches(0.8),
|
|
font=SERIF, size=22, color=OLIVE)
|
|
# 右栏标题(全色)
|
|
add_text(s, right_title,
|
|
Inches(7.0), Inches(1.2), Inches(5), Inches(0.8),
|
|
font=SERIF, size=22, color=NEAR_BLACK)
|
|
# 分隔线
|
|
add_line(s, Inches(1.2), Inches(2.2), Inches(11.5), weight_pt=0.5)
|
|
# 左栏条目(降调)
|
|
for i, item in enumerate(left_items[:4]):
|
|
add_text(s, item,
|
|
Inches(1.2), Inches(2.6 + i * 0.9), Inches(4.9), Inches(0.7),
|
|
font=SANS, size=17, color=STONE)
|
|
# 右栏条目(全色)
|
|
for i, item in enumerate(right_items[:4]):
|
|
add_text(s, item,
|
|
Inches(7.0), Inches(2.6 + i * 0.9), Inches(5.2), Inches(0.7),
|
|
font=SANS, size=17, color=DARK_WARM)
|
|
if page_num is not None:
|
|
add_text(s, f" - {page_num:02d}",
|
|
Inches(11.5), Inches(6.9), Inches(1.5), Inches(0.3),
|
|
font=SANS, size=11, color=STONE,
|
|
align=PP_ALIGN.RIGHT)
|
|
return s
|
|
|
|
|
|
def pipeline_slide(prs, eyebrow, title, steps, page_num=None):
|
|
"""流程步骤页:01/02/03 序号 + 步骤标题 + 步骤描述
|
|
steps: list of (step_title, step_desc),最多 4 步
|
|
"""
|
|
s = blank_slide(prs)
|
|
add_text(s, eyebrow,
|
|
Inches(1.2), Inches(0.6), Inches(10), Inches(0.4),
|
|
font=SANS, size=12, color=STONE)
|
|
add_text(s, title,
|
|
Inches(1.2), Inches(1.1), Inches(11), Inches(0.9),
|
|
font=SERIF, size=30, color=NEAR_BLACK)
|
|
add_line(s, Inches(1.2), Inches(2.15), Inches(11), weight_pt=0.5)
|
|
|
|
n = len(steps[:4])
|
|
step_w = Inches(11.5 / n)
|
|
for i, (step_title, step_desc) in enumerate(steps[:4]):
|
|
x = Inches(1.0) + step_w * i
|
|
# 序号
|
|
add_text(s, f"0{i+1}",
|
|
x, Inches(2.5), step_w, Inches(0.8),
|
|
font=SERIF, size=40, color=BRAND)
|
|
# 步骤标题
|
|
add_text(s, step_title,
|
|
x, Inches(3.45), step_w - Inches(0.2), Inches(0.6),
|
|
font=SERIF, size=19, color=NEAR_BLACK)
|
|
# 步骤描述
|
|
add_text(s, step_desc,
|
|
x, Inches(4.15), step_w - Inches(0.2), Inches(2.2),
|
|
font=SANS, size=15, color=OLIVE)
|
|
if page_num is not None:
|
|
add_text(s, f" - {page_num:02d}",
|
|
Inches(11.5), Inches(6.9), Inches(1.5), Inches(0.3),
|
|
font=SANS, size=11, color=STONE,
|
|
align=PP_ALIGN.RIGHT)
|
|
return s
|
|
|
|
|
|
def ending_slide(prs, message, contact):
|
|
"""结束页"""
|
|
s = blank_slide(prs)
|
|
add_text(s, message,
|
|
Inches(1), Inches(3), Inches(11.33), Inches(1.2),
|
|
font=SERIF, size=40, color=NEAR_BLACK,
|
|
align=PP_ALIGN.CENTER)
|
|
add_line(s, Inches(6.17), Inches(4.5), Inches(1), weight_pt=1.5)
|
|
add_text(s, contact,
|
|
Inches(1), Inches(4.8), Inches(11.33), Inches(0.6),
|
|
font=SANS, size=16, color=OLIVE,
|
|
align=PP_ALIGN.CENTER)
|
|
return s
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# Main: 示例 deck,按实际需求改
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--out", default="output.pptx",
|
|
help="Output PPTX path (default: output.pptx in cwd)")
|
|
args = parser.parse_args()
|
|
|
|
prs = Presentation()
|
|
prs.slide_width = SLIDE_W
|
|
prs.slide_height = SLIDE_H
|
|
|
|
# 1. 封面
|
|
cover_slide(prs,
|
|
title="{{文档标题}}",
|
|
subtitle="{{一句话描述}}",
|
|
author="{{作者}}",
|
|
date="2026.04")
|
|
|
|
# 2. 目录
|
|
toc_slide(prs, items=[
|
|
"{{章节 1}}",
|
|
"{{章节 2}}",
|
|
"{{章节 3}}",
|
|
"{{Q&A}}",
|
|
])
|
|
|
|
# 3. 章节首页
|
|
chapter_slide(prs, 1, "{{章节标题}}")
|
|
|
|
# 5. 内容页
|
|
content_slide(prs,
|
|
eyebrow="{{章节 · 本页}}",
|
|
title="{{核心论点标题}}",
|
|
body="{{一段正文,18pt sans 字体。控制在 3 行内,一屏一个核心信息。}}",
|
|
page_num=5)
|
|
|
|
# 7. 数据页
|
|
metrics_slide(prs,
|
|
title="关键结果",
|
|
metrics=[
|
|
("+42%", "转化率提升"),
|
|
("3.8M", "月活用户"),
|
|
("99.9%", "可用性 SLA"),
|
|
("5,000+", "QPS 峰值"),
|
|
])
|
|
|
|
# 6. 引用
|
|
quote_slide(prs,
|
|
quote="好的设计是尽可能少的设计。",
|
|
source="Dieter Rams")
|
|
|
|
# 8. 对比页
|
|
comparison_slide(prs,
|
|
eyebrow="{{章节 · 对比}}",
|
|
left_title="{{旧方案}}",
|
|
left_items=["{{对比点 A}}", "{{对比点 B}}", "{{对比点 C}}"],
|
|
right_title="{{新方案}}",
|
|
right_items=["{{改善点 A}}", "{{改善点 B}}", "{{改善点 C}}"],
|
|
page_num=8)
|
|
|
|
# 9. 流程步骤页
|
|
pipeline_slide(prs,
|
|
eyebrow="{{章节 · 流程}}",
|
|
title="{{核心流程标题}}",
|
|
steps=[
|
|
("{{步骤 1}}", "{{步骤 1 的说明文字,控制在两行内。}}"),
|
|
("{{步骤 2}}", "{{步骤 2 的说明文字,控制在两行内。}}"),
|
|
("{{步骤 3}}", "{{步骤 3 的说明文字,控制在两行内。}}"),
|
|
],
|
|
page_num=9)
|
|
|
|
# 7. 结束
|
|
ending_slide(prs,
|
|
message="Thank you",
|
|
contact="{{邮箱}} · {{网站}}")
|
|
|
|
prs.save(args.out)
|
|
print(f"OK: Saved {args.out}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|