1
0

feat: 实现幻灯片备注功能,将description写入PPT备注页

- 添加 PptxGenerator._set_notes() 方法设置备注
- 在 add_slide() 中调用 _set_notes() 处理 description
- 仅幻灯片级别的 description 写入备注,不继承模板
- 添加备注功能测试用例(8个测试)
- 更新 README.md 和 README_DEV.md 文档
- 新建 pptx-slide-notes spec
- 更新 page-description spec 允许写入备注
- 归档 add-slide-notes 变更
This commit is contained in:
2026-03-04 14:47:03 +08:00
parent f34405be36
commit 7ef29ea039
12 changed files with 454 additions and 15 deletions

View File

@@ -549,7 +549,7 @@ slides:
#### 幻灯片 description 字段
幻灯片可以包含可选的 `description` 字段,用于描述该幻灯片的作用和内容,仅用于文档目的,不影响生成的 PPTX 文件
幻灯片可以包含可选的 `description` 字段,用于描述该幻灯片的作用和内容。**`description` 内容会自动写入 PPT 备注页**,方便在演示时查看演讲说明
```yaml
slides:
@@ -564,6 +564,11 @@ slides:
content: "功能特性"
```
**注意事项**
- 仅幻灯片级别的 `description` 会写入备注
- 模板的 `description` 不会继承到幻灯片备注
- `metadata.description` 用于描述整个演示文稿,不写入单个幻灯片备注
### 条件渲染
#### 元素级条件渲染

View File

@@ -545,40 +545,48 @@ if right > slide_width + TOLERANCE:
**理由**
- 自文档化:提高模板和演示文稿的可读性和可维护性
- 不影响渲染:description 仅用于文档目的,不参与 PPTX 生成
- 备注支持:幻灯片 description 会写入 PPT 备注页,方便演讲者查看
- 完全向后兼容:字段为可选,现有文件无需修改
**实现要点**
1. **数据模型**
- `Presentation.description`:从 `metadata.description` 读取
- `Template.description`:从模板文件的 `description` 字段读取
- `Slide.description`:在 `render_slide()` 返回值中保留
- `Presentation.description`:从 `metadata.description` 读取,用于描述整个演示文稿
- `Template.description`:从模板文件的 `description` 字段读取,描述模板用途
- `Slide.description`:在 `render_slide()` 返回值中保留,会写入 PPT 备注页
2. **YAML 解析**
- 使用 `.get('description')` 自动处理缺失情况(返回 None
- YAML 原生支持多行文本格式
3. **不参与渲染**
- description 字段不传递给渲染器
- 不写入最终的 PPTX 文件
3. **PPT 备注功能**
- 仅幻灯片级别的 `description` 会写入 PPT 备注页
- 模板的 `description` 不继承到幻灯片备注
- `metadata.description` 不写入单个幻灯片备注
- 如果幻灯片没有 `description`,则不设置备注
4. **渲染实现**
- `PptxGenerator._set_notes()`:设置幻灯片备注的私有方法
- `PptxGenerator.add_slide()`:调用 `_set_notes()` 设置备注
**示例**
```yaml
# metadata description
# metadata description - 描述整个演示文稿
metadata:
description: "2024年度项目进展总结"
# 模板 description
# 模板 description - 描述模板用途
templates:
title-slide:
description: "用于章节标题页的模板"
elements: [...]
# 幻灯片 description
# 幻灯片 description - 写入 PPT 备注页
slides:
- description: "介绍项目背景"
- description: "介绍项目背景和目标,包含以下要点:..."
template: title-slide
vars:
title: "项目背景"
```
## 扩展指南

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-04

View File

@@ -0,0 +1,75 @@
## Context
当前系统中,`description` 字段已在以下位置被解析:
- `core/presentation.py`: 演示文稿级别的 `metadata.description`
- `core/template.py`: 模板级别的 `template.description`
- `core/presentation.py`: 幻灯片级别的 `slide.description``render_slide` 方法返回值中
然而PPTX 渲染器 (`renderers/pptx_renderer.py`) 的 `add_slide` 方法只处理 `background``elements`,完全忽略了 `description` 字段。
python-pptx 库原生支持备注功能,通过 `slide.notes_slide.notes_text_frame.text` 即可设置。
## Goals / Non-Goals
**Goals:**
- 将幻灯片的 `description` 字段写入 PPT 备注页
- 仅处理幻灯片级别的 description不涉及模板或演示文稿级别的 description
- 保持向后兼容,没有 description 时不设置备注
**Non-Goals:**
- 不实现富文本备注(仅支持纯文本)
- 不合并模板的 description
- 不处理演示文稿级别的 metadata.description
## Decisions
### 1. 备注内容来源
**决策**: 仅使用幻灯片自己的 `description` 字段
**理由**:
- 幻灯片 description 是用户针对具体页面编写的说明
- 模板 description 描述的是模板用途,不适合作为单个幻灯片的备注
- 避免复杂的合并逻辑,保持简单清晰
**考虑的替代方案**:
- 合并模板 description 和幻灯片 description → 过于复杂,可能产生冗余内容
- 仅在没有幻灯片 description 时使用模板 description → 增加认知负担
### 2. 无 description 时的行为
**决策**: 不设置备注,保持备注页为空
**理由**:
- python-pptx 默认创建空备注页
- 不需要额外判断或清理操作
- 保持行为可预测
### 3. 代码位置
**决策**: 在 `PptxGenerator` 类中添加私有方法 `_set_notes`
**理由**:
- 与现有的 `_render_background``_render_element` 等方法保持一致
- 封装备注设置逻辑,便于测试和维护
- 不影响 `add_slide` 方法的主流程
## Risks / Trade-offs
### Risk: 长文本可能导致备注溢出
**影响**: description 内容过长时,可能在备注页中显示不佳
**缓解**: python-pptx 会自动处理文本换行,这是库的默认行为,无需额外处理
### Trade-off: 仅支持纯文本
**限制**: python-pptx 的备注不支持富文本格式(加粗、颜色等)
**权衡**: 备注的主要用途是演讲者参考,纯文本已满足需求;富文本支持需要更复杂的实现,收益不大
### Risk: 现有 spec 需要更新
**影响**: `page-description` spec 明确规定 "description不写入PPTX文件"
**缓解**: 这正是本次变更的目的,需要更新该 spec 以反映新行为

View File

@@ -0,0 +1,25 @@
## Why
幻灯片的 description 字段已经在系统中被解析和传递但目前未被实际使用。PPT 备注页是演讲者的原生工具,非常适合存储这些演讲说明。通过将 description 写入 PPT 备注,可以让用户在演示时查看说明而不显示给观众,充分利用已有数据。
## What Changes
- 在 PPTX 生成时,将幻灯片的 `description` 字段写入备注页
- 仅处理幻灯片级别的 `description`,不继承模板的 `description`
- 如果幻灯片没有 `description`,则不设置备注
- 修改现有 `page-description` spec 的需求,因为 description 现在会影响 PPTX 输出
## Capabilities
### New Capabilities
- `pptx-slide-notes`: 幻灯片备注功能,支持将 description 内容写入 PPT 备注页
### Modified Capabilities
- `page-description`: 现有 spec 规定 "description字段不得影响渲染逻辑" 和 "description不写入PPTX文件",需要更新为允许 description 写入备注页
## Impact
- `renderers/pptx_renderer.py`: 在 `add_slide` 方法中添加设置备注的逻辑
- `openspec/specs/page-description/spec.md`: 更新需求,移除 "不得影响渲染逻辑" 的限制
- `openspec/specs/pptx-slide-notes/spec.md`: 新建 spec 定义备注功能需求
- `README.md``README_DEV.md`: 更新文档说明备注功能

View File

@@ -0,0 +1,23 @@
## MODIFIED Requirements
### Requirement: description字段不得影响渲染逻辑
系统 SHALL 在渲染幻灯片时忽略 `description` 字段对视觉元素的影响,但会将幻灯片级别的 `description` 写入 PPTX 备注页。
#### Scenario: 渲染包含description的模板
- **WHEN** 系统渲染包含 `description` 字段的模板
- **THEN** description不参与元素渲染不影响幻灯片视觉输出
#### Scenario: 渲染包含description的幻灯片
- **WHEN** 系统渲染包含 `description` 字段的幻灯片
- **THEN** description写入PPTX文件的备注页不影响幻灯片视觉输出
## REMOVED Requirements
### Requirement: description不写入PPTX文件
**Reason**: 幻灯片备注功能需要将 description 写入 PPTX 备注页,这是对 description 字段的合理利用。
**Migration**: 无需迁移,这是新增功能,不影响现有行为。没有 description 的幻灯片行为与之前完全一致。

View File

@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: 幻灯片description必须写入PPT备注页
系统 SHALL 在生成 PPTX 时,将幻灯片的 `description` 字段内容写入该幻灯片的备注页。
#### Scenario: 幻灯片包含description
- **WHEN** 幻灯片定义了 `description: "这是幻灯片的演讲说明"`
- **THEN** PPTX 幻灯片的备注页包含该文本内容
#### Scenario: description包含多行文本
- **WHEN** 幻灯片的 description 使用 YAML 多行文本格式定义
- **THEN** 备注页保留完整的文本内容,包括换行符
#### Scenario: description为空字符串
- **WHEN** 幻灯片定义了 `description: ""`
- **THEN** 备注页设置为空字符串(不为 None
### Requirement: 无description时不设置备注
系统 SHALL 在幻灯片没有 `description` 字段时不设置备注内容。
#### Scenario: 幻灯片不包含description
- **WHEN** 幻灯片定义未包含 `description` 字段
- **THEN** PPTX 幻灯片不设置备注,使用默认空备注页
### Requirement: 模板description不写入备注
系统 SHALL 仅处理幻灯片级别的 `description`,不使用模板的 `description`
#### Scenario: 模板有description但幻灯片没有
- **WHEN** 模板定义了 `description: "模板说明"` 但幻灯片未定义 `description`
- **THEN** PPTX 幻灯片不设置备注,不继承模板的 description
#### Scenario: 模板和幻灯片都有description
- **WHEN** 模板定义了 `description` 且幻灯片也定义了 `description`
- **THEN** PPTX 幻灯片备注仅包含幻灯片的 description忽略模板的 description
### Requirement: description必须支持中文字符
系统 SHALL 支持在 `description` 字段中使用中文字符,并正确写入 PPTX 备注。
#### Scenario: description包含中文
- **WHEN** 幻灯片的 description 包含中文字符,如 "这是演讲备注内容"
- **THEN** 系统正确处理PPTX 备注页显示正确的中文内容

View File

@@ -0,0 +1,17 @@
## 1. 核心实现
- [x] 1.1 在 PptxGenerator 类中添加 _set_notes 私有方法
- [x] 1.2 在 add_slide 方法中调用 _set_notes 设置备注
## 2. 测试
- [x] 2.1 添加幻灯片包含 description 的测试用例
- [x] 2.2 添加幻灯片不包含 description 的测试用例
- [x] 2.3 添加 description 包含多行文本的测试用例
- [x] 2.4 添加 description 包含中文字符的测试用例
- [x] 2.5 添加模板有 description 但幻灯片没有的测试用例
## 3. 文档更新
- [x] 3.1 更新 README.md 添加备注功能说明
- [x] 3.2 更新 README_DEV.md 添加备注功能开发文档

View File

@@ -60,17 +60,17 @@ Page Description功能允许用户为文档元数据、模板和幻灯片添加
### Requirement: description字段不得影响渲染逻辑
系统 SHALL 在渲染过程中忽略 `description` 字段不影响最终的PPTX输出
系统 SHALL 在渲染幻灯片时忽略 `description` 字段对视觉元素的影响,但会将幻灯片级别的 `description` 写入 PPTX 备注页
#### Scenario: 渲染包含description的模板
- **WHEN** 系统渲染包含 `description` 字段的模板
- **THEN** description不参与元素渲染不影响输出结果
- **THEN** description不参与元素渲染不影响幻灯片视觉输出
#### Scenario: 渲染包含description的幻灯片
- **WHEN** 系统渲染包含 `description` 字段的幻灯片
- **THEN** description写入PPTX文件,不影响输出结果
- **THEN** description写入PPTX文件的备注页,不影响幻灯片视觉输出
### Requirement: YAML解析器必须正确解析description字段

View File

@@ -0,0 +1,58 @@
# PPTX Slide Notes
## Purpose
PPTX 备注功能允许用户在生成 PPTX 文件时,将幻灯片的 description 字段内容写入备注页。备注页是 PPT 的原生演讲者工具,在演示时可以查看但不显示给观众,非常适合存储演讲说明和备注信息。
## Requirements
### Requirement: 幻灯片description必须写入PPT备注页
系统 SHALL 在生成 PPTX 时,将幻灯片的 `description` 字段内容写入该幻灯片的备注页。
#### Scenario: 幻灯片包含description
- **WHEN** 幻灯片定义了 `description: "这是幻灯片的演讲说明"`
- **THEN** PPTX 幻灯片的备注页包含该文本内容
#### Scenario: description包含多行文本
- **WHEN** 幻灯片的 description 使用 YAML 多行文本格式定义
- **THEN** 备注页保留完整的文本内容,包括换行符
#### Scenario: description为空字符串
- **WHEN** 幻灯片定义了 `description: ""`
- **THEN** 备注页设置为空字符串(不为 None
### Requirement: 无description时不设置备注
系统 SHALL 在幻灯片没有 `description` 字段时不设置备注内容。
#### Scenario: 幻灯片不包含description
- **WHEN** 幻灯片定义未包含 `description` 字段
- **THEN** PPTX 幻灯片不设置备注,使用默认空备注页
### Requirement: 模板description不写入备注
系统 SHALL 仅处理幻灯片级别的 `description`,不使用模板的 `description`
#### Scenario: 模板有description但幻灯片没有
- **WHEN** 模板定义了 `description: "模板说明"` 但幻灯片未定义 `description`
- **THEN** PPTX 幻灯片不设置备注,不继承模板的 description
#### Scenario: 模板和幻灯片都有description
- **WHEN** 模板定义了 `description` 且幻灯片也定义了 `description`
- **THEN** PPTX 幻灯片备注仅包含幻灯片的 description忽略模板的 description
### Requirement: description必须支持中文字符
系统 SHALL 支持在 `description` 字段中使用中文字符,并正确写入 PPTX 备注。
#### Scenario: description包含中文
- **WHEN** 幻灯片的 description 包含中文字符,如 "这是演讲备注内容"
- **THEN** 系统正确处理PPTX 备注页显示正确的中文内容

View File

@@ -60,6 +60,11 @@ class PptxGenerator:
for elem in elements:
self._render_element(slide, elem, base_path)
# 设置备注
description = slide_data.get('description')
if description:
self._set_notes(slide, description)
def _render_element(self, slide, elem, base_path):
"""
分发元素到对应的渲染方法
@@ -269,6 +274,20 @@ class PptxGenerator:
from utils import log_info
log_info(f"图片背景暂未实现: {background['image']}")
def _set_notes(self, slide, text):
"""
设置幻灯片备注
Args:
slide: pptx slide 对象
text: 备注文本内容
"""
if text is None:
return
notes_slide = slide.notes_slide
text_frame = notes_slide.notes_text_frame
text_frame.text = text
def save(self, output_path):
"""
保存 PPTX 文件

View File

@@ -540,3 +540,158 @@ class TestRenderBackground:
# 不应该崩溃
gen._render_background(mock_slide, None)
class TestSetNotes:
"""_set_notes 方法测试类"""
@patch("renderers.pptx_renderer.PptxPresentation")
def test_set_notes_with_text(self, mock_prs_class):
"""测试设置备注文本"""
mock_slide = Mock()
mock_notes_slide = Mock()
mock_text_frame = Mock()
mock_notes_slide.notes_text_frame = mock_text_frame
mock_slide.notes_slide = mock_notes_slide
mock_prs_class.return_value = Mock()
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
gen = PptxGenerator()
gen._set_notes(mock_slide, "这是演讲备注内容")
# 验证备注被设置
mock_text_frame.text = "这是演讲备注内容"
@patch("renderers.pptx_renderer.PptxPresentation")
def test_set_notes_with_none(self, mock_prs_class):
"""测试设置 None 不设置备注"""
mock_slide = Mock(spec=[]) # 使用 spec=[] 禁止自动创建属性
mock_prs_class.return_value = Mock()
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
gen = PptxGenerator()
# 不应该崩溃,不应该访问 notes_slide
gen._set_notes(mock_slide, None)
# 使用 spec=[] 后,访问不存在的属性会抛出 AttributeError
# 如果没有抛出异常,说明 notes_slide 没有被访问
@patch("renderers.pptx_renderer.PptxPresentation")
def test_set_notes_with_empty_string(self, mock_prs_class):
"""测试设置空字符串"""
mock_slide = Mock()
mock_notes_slide = Mock()
mock_text_frame = Mock()
mock_notes_slide.notes_text_frame = mock_text_frame
mock_slide.notes_slide = mock_notes_slide
mock_prs_class.return_value = Mock()
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
gen = PptxGenerator()
gen._set_notes(mock_slide, "")
# 验证空字符串也被设置
mock_text_frame.text = ""
@patch("renderers.pptx_renderer.PptxPresentation")
def test_set_notes_with_multiline_text(self, mock_prs_class):
"""测试设置多行文本"""
mock_slide = Mock()
mock_notes_slide = Mock()
mock_text_frame = Mock()
mock_notes_slide.notes_text_frame = mock_text_frame
mock_slide.notes_slide = mock_notes_slide
mock_prs_class.return_value = Mock()
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
gen = PptxGenerator()
multiline_text = "第一行备注\n第二行备注\n第三行备注"
gen._set_notes(mock_slide, multiline_text)
# 验证多行文本被设置
mock_text_frame.text = multiline_text
@patch("renderers.pptx_renderer.PptxPresentation")
def test_set_notes_with_chinese_text(self, mock_prs_class):
"""测试设置中文备注"""
mock_slide = Mock()
mock_notes_slide = Mock()
mock_text_frame = Mock()
mock_notes_slide.notes_text_frame = mock_text_frame
mock_slide.notes_slide = mock_notes_slide
mock_prs_class.return_value = Mock()
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
gen = PptxGenerator()
chinese_text = "这是中文演讲备注内容,用于演示时参考"
gen._set_notes(mock_slide, chinese_text)
# 验证中文文本被设置
mock_text_frame.text = chinese_text
class TestAddSlideWithNotes:
"""add_slide 方法备注功能测试类"""
@patch("renderers.pptx_renderer.PptxPresentation")
def test_add_slide_with_description_sets_notes(self, mock_prs_class):
"""测试幻灯片包含 description 时设置备注"""
mock_slide = Mock()
mock_notes_slide = Mock()
mock_text_frame = Mock()
mock_notes_slide.notes_text_frame = mock_text_frame
mock_slide.notes_slide = mock_notes_slide
mock_prs = Mock()
mock_prs.slide_layouts = [None] * 6 + [Mock()] + [None]
mock_prs.slides.add_slide.return_value = mock_slide
mock_prs_class.return_value = mock_prs
gen = PptxGenerator()
slide_data = {"background": None, "elements": [], "description": "这是幻灯片的演讲说明"}
gen.add_slide(slide_data)
# 验证备注被设置
mock_text_frame.text = "这是幻灯片的演讲说明"
@patch("renderers.pptx_renderer.PptxPresentation")
def test_add_slide_without_description_no_notes(self, mock_prs_class):
"""测试幻灯片不包含 description 时不设置备注"""
mock_slide = Mock(spec=[]) # 禁止自动创建属性
mock_prs = Mock()
mock_prs.slide_layouts = [None] * 6 + [Mock()] + [None]
mock_prs.slides.add_slide.return_value = mock_slide
mock_prs_class.return_value = mock_prs
gen = PptxGenerator()
slide_data = {"background": None, "elements": []}
gen.add_slide(slide_data)
# 不应该访问 notes_slide使用 spec=[] 会抛出异常如果被访问)
@patch("renderers.pptx_renderer.PptxPresentation")
def test_add_slide_with_template_description_ignored(self, mock_prs_class):
"""测试模板的 description 不被用作备注(仅幻灯片自己的 description 有效)"""
mock_slide = Mock(spec=[]) # 禁止自动创建属性
mock_prs = Mock()
mock_prs.slide_layouts = [None] * 6 + [Mock()] + [None]
mock_prs.slides.add_slide.return_value = mock_slide
mock_prs_class.return_value = mock_prs
gen = PptxGenerator()
# 幻灯片没有 description则不设置备注即使模板可能有 description
slide_data = {"background": None, "elements": []}
gen.add_slide(slide_data)
# 不应该访问 notes_slide使用 spec=[] 会抛出异常如果被访问)