zcbot/skills/ppt/references/layouts.md

367 lines
14 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 校验不会越出画布。
复制 → 改文案 → 跑。配色用 `spec_lock.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_lock 里有覆盖以 spec_lock 为准) ----
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")
```
> **要点**:
> - `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)
# 左上角小色块 + 标题左侧细色条
add_rect(slide, 0.7, 0.7, 0.6, 0.06, PRIMARY) # 顶部短线
add_rect(slide, 0.7, 1.05, 0.06, 1.5, ACCENT) # 左侧竖线 (装饰)
# 主标题
add_textbox(slide, 0.7, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
44, bold=True, color=INK, name="cover_title")
# 副标题 (灰色,弱化)
add_textbox(slide, 0.7, 4.1, 11.9, 0.6, "一句话副标题或定位",
22, color=GREY, name="cover_sub")
# 汇报人 / 日期
add_textbox(slide, 0.7, 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)
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)
# 整页极浅灰 (替代深色满铺)
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG)
# 左侧装饰竖条
add_rect(slide, 0.7, 2.5, 0.08, 2.5, ACCENT)
# 大编号 (主色,描边视觉感)
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)
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)
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)
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)
# 左上大引号 (用 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 —— 浅色 + 大字,不再满铺深色
```python
slide = prs.slides.add_slide(BLANK)
# 顶部 + 底部装饰短线 (代替整页色块)
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "top_line")
add_rect(slide, SAFE_RIGHT - 0.8, 6.85, 0.8, 0.06, ACCENT, "bottom_line")
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 一项**