zcbot/skills/ppt/references/layouts.md

398 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 9 种常用版式 (16:9, 13.33×7.5 in)
> **2.0 版本要点**:大幅减少满铺色块,引入 MSO_SHAPE 图标点缀,所有元素经 safe_area 校验不会越出画布。
复制 → 改文案 → 跑。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex 替换占位。
## 通用起手 + 安全辅助
```python
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_SHAPE
# ---- 配色 (商务红 — 硬约束默认) ----
# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec 已写其它 hex,
# 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。
PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据
SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形
ACCENT = RGBColor(0xFF, 0xC1, 0x07) # 金黄 - 关键数据点/CTA
INK = RGBColor(0x1F, 0x1F, 0x1F)
GREY = RGBColor(0x59, 0x59, 0x59)
GREY_LIGHT = RGBColor(0x88, 0x88, 0x88)
BG = RGBColor(0xFA, 0xFA, 0xFA) # 背景近白
WHITE = RGBColor(255, 255, 255)
CN_FONT = "微软雅黑"
EN_FONT = "Arial"
# ---- 画布与安全区 ----
prs = Presentation()
prs.slide_width = Inches(13.33)
prs.slide_height = Inches(7.5)
SLIDE_W = 13.33
SLIDE_H = 7.5
MARGIN_X = 0.7 # 左右
MARGIN_Y = 0.5 # 上下
SAFE_LEFT = MARGIN_X
SAFE_TOP = MARGIN_Y
SAFE_RIGHT = SLIDE_W - MARGIN_X
SAFE_BOTTOM = SLIDE_H - MARGIN_Y
SAFE_W = SAFE_RIGHT - SAFE_LEFT # 11.93
SAFE_H = SAFE_BOTTOM - SAFE_TOP # 6.5
BLANK = prs.slide_layouts[6]
def assert_inside(left, top, width, height, name=""):
"""放置前调一次。越界直接报错而不是悄悄超出。"""
if left < 0 or top < 0:
raise ValueError(f"[{name}] 左/上为负: ({left}, {top})")
if left + width > SLIDE_W + 1e-3:
raise ValueError(f"[{name}] 右越界: {left}+{width} > {SLIDE_W}")
if top + height > SLIDE_H + 1e-3:
raise ValueError(f"[{name}] 下越界: {top}+{height} > {SLIDE_H}")
# ---- 文本辅助 (默认 word_wrap, shrink-to-fit 兜底) ----
def set_text(tf, text, size, bold=False, color=INK, align=PP_ALIGN.LEFT,
font=CN_FONT):
tf.text = text
p = tf.paragraphs[0]; p.alignment = align
r = p.runs[0]
r.font.name = font; r.font.size = Pt(size); r.font.bold = bold
r.font.color.rgb = color
def add_textbox(slide, left, top, width, height, text, size,
bold=False, color=INK, align=PP_ALIGN.LEFT,
anchor=MSO_ANCHOR.TOP, font=CN_FONT, shrink=True,
name="textbox"):
assert_inside(left, top, width, height, name)
tb = slide.shapes.add_textbox(Inches(left), Inches(top),
Inches(width), Inches(height))
tf = tb.text_frame
tf.vertical_anchor = anchor
tf.word_wrap = True
if shrink:
# 文字超出框高时自动收缩字号 (兜底,不替代字数预算)
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
set_text(tf, text, size, bold, color, align, font)
return tb
# ---- 形状辅助 (无边线实心填充) ----
def add_rect(slide, left, top, width, height, fill, name="rect"):
assert_inside(left, top, width, height, name)
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
Inches(width), Inches(height))
s.fill.solid(); s.fill.fore_color.rgb = fill
s.line.fill.background()
return s
def add_shape(slide, kind, left, top, width, height, fill, name="shape"):
assert_inside(left, top, width, height, name)
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
Inches(width), Inches(height))
s.fill.solid(); s.fill.fore_color.rgb = fill
s.line.fill.background()
return s
def add_dot(slide, x, y, size=0.18, color=ACCENT):
return add_shape(slide, MSO_SHAPE.OVAL, x, y, size, size, color, "dot")
def add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=ACCENT):
"""标题下面那条强调线,替代大色块"""
return add_rect(slide, x, y, length, thickness, color, "accent_line")
def add_badge(slide, x, y, num, diameter=0.7, fill=PRIMARY, fg=WHITE):
"""编号徽章 (圆 + 数字)"""
c = add_shape(slide, MSO_SHAPE.OVAL, x, y, diameter, diameter, fill, "badge")
tf = c.text_frame; tf.text = str(num)
p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
r = p.runs[0]
r.font.bold = True; r.font.size = Pt(int(diameter * 28))
r.font.color.rgb = fg; r.font.name = EN_FONT
return c
# ---- 标题套件 (内页通用) ----
def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
add_textbox(slide, SAFE_LEFT, SAFE_TOP, SAFE_W, 0.7, text,
32, bold=True, color=PRIMARY, name="title")
add_accent_line(slide, SAFE_LEFT, SAFE_TOP + 0.85,
length=0.8, color=ACCENT)
if page_num is not None and total is not None:
add_textbox(slide, SAFE_LEFT, 7.0, 6, 0.4, footer,
11, color=GREY_LIGHT, shrink=False, name="footer")
add_textbox(slide, 12.0, 7.0, 1.2, 0.4, f"{page_num} / {total}",
11, color=GREY_LIGHT, align=PP_ALIGN.RIGHT,
shrink=False, name="page_num")
# ---- 品牌条 (每页起手必调,确保不是裸白纸) ----
def apply_brand(slide, kind="inner"):
"""统一品牌锚点。每个版式第一行调用,给一条窄的主色锚点 + 必要时浅底。
kind:
cover —— 封面: 左侧主色长竖条 + 顶部短横
inner —— 内页 (默认): 左侧主色窄条 (从标题到底部)
section —— 分章: 整页浅灰 + 左侧强调色粗竖条
end —— 结尾: 整页浅灰 + 顶/底强调色短线
"""
if kind == "cover":
# 顶部短主色横线 + 左侧主色长竖条 + 底部细灰线
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line")
add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02,
RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline")
elif kind == "section":
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
add_rect(slide, 0.7, 2.5, 0.08, 2.5, ACCENT, "brand_section_bar")
elif kind == "end":
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
add_rect(slide, SAFE_RIGHT - 0.8, 6.85, 0.8, 0.06, ACCENT,
"brand_btm_line")
else: # inner — 默认内页
# 左侧主色窄条 (从标题区到页脚)
add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
# 底部一条细灰线 (页脚分隔)
add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02,
RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline")
```
> **要点**:
> - `assert_inside` 阻止任何越界。元素超出画布会立刻报 `ValueError`,而不是悄悄裁剪
> - `add_textbox` 默认 `word_wrap=True` + `auto_size=TEXT_TO_SHAPE_AND_FIT_TEXT` —— 文字溢出自动缩字号
> - `page_title` 用细线代替大块色填,所有内页统一调用
---
## L1 · 封面 (Cover) —— 主色长竖条锚点
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横
# 主标题 (避开左竖条)
add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
44, bold=True, color=INK, name="cover_title")
# 副标题 (灰色,弱化)
add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位",
22, color=GREY, name="cover_sub")
# 汇报人 / 日期
add_textbox(slide, 0.9, 6.4, 11.9, 0.4,
"汇报人 · 部门 · 2026-05-06", 14, color=GREY_LIGHT,
name="cover_meta")
# 右下角小图标点缀 (五角星,可选)
add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, ACCENT,
"deco_star")
```
---
## L2 · 目录 (Agenda) —— 编号徽章 + 文字
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "inner")
page_title(slide, "目录")
items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"]
for i, item in enumerate(items):
y = 1.9 + i * 0.95
add_badge(slide, SAFE_LEFT, y, i + 1, diameter=0.65)
add_textbox(slide, SAFE_LEFT + 1.0, y, SAFE_W - 1.0, 0.65,
item, 22, color=INK, anchor=MSO_ANCHOR.MIDDLE,
name=f"agenda_{i}")
```
---
## L3 · 章节分隔 (Section Divider) —— 浅色背景 + 大字编号
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰
# 大编号 (主色,描边视觉感)
add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True,
color=PRIMARY, font=EN_FONT, name="sec_num")
# 章节名
add_textbox(slide, 5.5, 2.8, 7, 1.0, "背景与现状",
44, bold=True, color=INK, anchor=MSO_ANCHOR.MIDDLE,
name="sec_title")
# 引言
add_textbox(slide, 5.5, 4.0, 7, 0.6,
"本章讨论行业现状与机会窗口", 18, color=GREY,
name="sec_lead")
# 装饰小图标
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, ACCENT,
"sec_arrow")
```
---
## L4 · 要点 (Bullets) —— 圆点 + 文字,无大块色
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "inner")
page_title(slide, "核心结论")
bullets = [
"结论一:用一句话讲清楚",
"结论二:具体数据支撑,如增长 27%",
"结论三:对未来的判断,简洁有力",
"结论四:可选第四条,不要超过 5 条",
]
for i, b in enumerate(bullets):
y = 2.0 + i * 0.95
add_dot(slide, SAFE_LEFT + 0.05, y + 0.22, size=0.18, color=ACCENT)
add_textbox(slide, SAFE_LEFT + 0.45, y, SAFE_W - 0.45, 0.6,
b, 22, color=INK, anchor=MSO_ANCHOR.MIDDLE,
name=f"bullet_{i}")
```
---
## L5 · 双栏对比 (Two-Column) —— 中线分隔,小色块标签
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "inner")
page_title(slide, "现状 vs 改进后")
mid_x = SLIDE_W / 2
# 中间细分隔线 (替代两块大矩形)
add_rect(slide, mid_x - 0.02, 2.0, 0.04, 4.8, RGBColor(0xDD, 0xDD, 0xDD),
"divider")
# 左栏小标签 (色块只占小区域)
add_rect(slide, SAFE_LEFT, 2.0, 0.8, 0.35, GREY, "left_tag")
add_textbox(slide, SAFE_LEFT, 2.0, 0.8, 0.35, "现状", 14, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
shrink=False, name="left_label")
left_pts = ["问题 A: 描述", "问题 B: 描述", "问题 C: 描述"]
for i, p in enumerate(left_pts):
add_dot(slide, SAFE_LEFT + 0.05, 2.7 + i * 0.7 + 0.18, color=GREY)
add_textbox(slide, SAFE_LEFT + 0.45, 2.7 + i * 0.7,
mid_x - SAFE_LEFT - 0.7, 0.55, p, 18, color=INK,
anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
# 右栏小标签
add_rect(slide, mid_x + 0.3, 2.0, 0.8, 0.35, PRIMARY, "right_tag")
add_textbox(slide, mid_x + 0.3, 2.0, 0.8, 0.35, "改进后", 14, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
shrink=False, name="right_label")
right_pts = ["改善 A: 描述", "改善 B: 描述", "改善 C: 描述"]
for i, p in enumerate(right_pts):
add_dot(slide, mid_x + 0.35, 2.7 + i * 0.7 + 0.18, color=ACCENT)
add_textbox(slide, mid_x + 0.75, 2.7 + i * 0.7,
SAFE_RIGHT - mid_x - 0.75, 0.55, p, 18, color=INK,
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
```
---
## L6 · 图表为主 (Chart-focus) —— 标题 + 一句结论 + 大图
```python
# chart.png 已用 matplotlib 生成 (见 design_principles.md §7)
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "inner")
page_title(slide, "季度营收持续增长")
# 一句话结论
add_textbox(slide, SAFE_LEFT, SAFE_TOP + 1.1, SAFE_W, 0.5,
"Q4 同比增长 158%,创历史新高", 18, color=GREY,
name="lead")
# 图表 (居中,占 9 寸宽,高度自适应)
slide.shapes.add_picture("chart.png", Inches(2.2), Inches(2.4),
width=Inches(8.9))
# 数据来源 (右下角弱化)
add_textbox(slide, SAFE_LEFT, 6.95, SAFE_W, 0.4,
"数据来源: 公司年报 2025", 11, color=GREY_LIGHT,
align=PP_ALIGN.RIGHT, shrink=False, name="source")
```
---
## L7 · 图片为主 (Image-focus) —— 文字在图旁,不压图
> 之前用满铺图 + 半透明遮罩,效果不稳定。改成"图占 60% + 文字独立区"。
```python
slide = prs.slides.add_slide(BLANK)
# 左侧图占 60% 宽
slide.shapes.add_picture("hero.jpg", Inches(0), Inches(0),
width=Inches(8), height=Inches(7.5))
# 右侧浅灰背景区放文字
add_rect(slide, 8, 0, 5.33, 7.5, BG, "text_panel")
add_rect(slide, 8.4, 1.0, 0.06, 0.8, ACCENT, "deco_bar") # 装饰短线
add_textbox(slide, 8.4, 2.0, 4.6, 1.6, "走进未来", 36,
bold=True, color=INK, name="img_title")
add_textbox(slide, 8.4, 3.8, 4.6, 1.5,
"用一两句话点出主旨,不要把演讲稿搬上来。",
18, color=GREY, name="img_caption")
# 图标:右下角的箭头,引导视线
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, ACCENT,
"img_cta")
```
---
## L8 · 金句 / 大字 (Quote) —— 留白主导,装饰极简
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "inner")
# 左上大引号 (用 STAR 不合适;用字形)
add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True,
color=ACCENT, font=EN_FONT, shrink=False, name="quote_mark")
# 金句 (深色,留白多)
add_textbox(slide, 1.5, 2.7, 10.5, 2.0,
"把复杂留给我们,把简单留给用户。", 36, bold=True,
color=INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
# 装饰短线
add_accent_line(slide, 1.5, 5.0, length=0.5, color=ACCENT)
# 出处
add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
16, color=GREY, name="quote_attr")
```
---
## L9 · 结尾 / Q&A —— 浅底 + 大字,**强制必有**
> **不是可选** —— 任何 deck 都必须以这页收尾。无论是汇报、提案、路演,缺尾页等于"话没说完"。
```python
slide = prs.slides.add_slide(BLANK)
apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线
add_textbox(slide, 0, 2.5, SLIDE_W, 1.6, "Thank You", 80, bold=True,
color=PRIMARY, align=PP_ALIGN.CENTER, font=EN_FONT,
name="thanks")
add_textbox(slide, 0, 4.3, SLIDE_W, 0.6, "欢迎提问与讨论",
22, color=ACCENT, align=PP_ALIGN.CENTER, name="qa")
add_textbox(slide, 0, 6.2, SLIDE_W, 0.5,
"联系方式 / 邮箱 / 公众号", 14, color=GREY_LIGHT,
align=PP_ALIGN.CENTER, name="contact")
```
---
## 选版式速查
```
有数据 ≥ 3 点 → L6 (Chart-focus)
对比类 (前/后, A/B) → L5 (Two-Column)
要点 ≤ 5 条 → L4 (Bullets)
转场 / 换章 → L3 (Section Divider)
首页 → L1 (Cover)
末页 → L9 (Q&A)
有大图 / 视觉优先 → L7 (Image-focus)
观点强调 / 名言 → L8 (Quote)
```
## 三个常犯的越界场景
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字。超过 1 行就溢出 0.7 in 高的框。**用 `assert_inside` + `auto_size=TEXT_TO_SHAPE_AND_FIT_TEXT` 兜底**;但根本解法是**字数压缩**(见 design_principles.md §字数预算)
2. **标题占两行** —— 标题在 0.7 in 高的框里,32pt 单行高约 0.45 in,**两行就溢出**。中文标题 ≤ 30 字
3. **图片不等比拉伸** —— `add_picture(width=, height=)` 同时给会变形;**只给 width 或 height 一项**