1
0

feat: 实现字体主题系统和东亚字体支持

实现完整的字体主题系统,支持可复用字体配置、预设类别和扩展属性。
同时修复中文字体渲染问题,确保 Source Han Sans 等东亚字体正确显示。

核心功能:
- 字体主题配置:metadata.fonts 和 fonts_default
- 三种引用方式:整体引用、继承覆盖、独立定义
- 预设字体类别:sans、serif、mono、cjk-sans、cjk-serif
- 扩展字体属性:family、underline、strikethrough、line_spacing、
  space_before、space_after、baseline、caps
- 表格字体字段:font 和 header_font 替代旧的 style.font_size
- 引用循环检测和属性继承链
- 模板字体继承支持

东亚字体修复:
- 添加 _set_font_with_eastasian() 方法
- 同时设置拉丁字体、东亚字体和复杂脚本字体
- 修复中文字符使用默认字体的问题

测试:
- 58 个单元测试覆盖所有字体系统功能
- 3 个集成测试验证端到端场景
- 移除旧语法相关测试

文档:
- 更新 README.md 添加字体主题系统使用说明
- 更新 README_DEV.md 添加技术文档
- 创建 4 个示例 YAML 文件
- 同步 delta specs 到主 specs

归档:
- 归档 font-theme-system 变更到 openspec/changes/archive/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 10:38:59 +08:00
parent 7ef29ea039
commit bd12fce14b
22 changed files with 3142 additions and 103 deletions

View File

@@ -111,17 +111,42 @@ Element rendering系统负责将 YAML 中定义的各类元素(文本、图片
### Requirement: 系统必须支持表格元素渲染
系统 SHALL 将 YAML 中定义的表格元素渲染为 PPTX 表格对象。
系统 SHALL 将 YAML 中定义的表格元素渲染为 PPTX 表格对象。表格元素支持 font 和 header_font 字段用于字体配置style 字段仅用于非字体样式(如背景色)。
#### Scenario: 渲染基本表格
- **WHEN** 元素定义为 `{type: table, position: [1, 2], data: [["A", "B"], ["C", "D"]], col_widths: [2, 2]}`
- **THEN** 系统在 (1, 2) 位置创建 2×2 表格,列宽各为 2 英寸
#### Scenario: 应用表格样式
#### Scenario: 表格定义整体字体
- **WHEN** 表格定义 `style: {font_size: 14, header_bg: "#4a90e2", header_color: "#ffffff"}`
- **THEN** 系统将第一行设置为表头样式,背景色为 #4a90e2,文字颜色为白色
- **WHEN** 表格定义 `font: {family: "Arial", size: 14, color: "#333333"}`
- **THEN** 系统将数据单元格和表头都应用该字体样式
#### Scenario: 表格定义表头字体
- **WHEN** 表格定义 `header_font: {bold: true, color: "#ffffff"}`
- **THEN** 系统将表头应用该字体样式,数据单元格继承 font 或使用默认字体
#### Scenario: 表头字体继承表格字体
- **WHEN** 表格定义 `font: {size: 14}``header_font: {parent: "@font", bold: true}`
- **THEN** 表头继承 size: 14覆盖 bold: true
#### Scenario: 表格仅定义 header_font
- **WHEN** 表格仅定义 `header_font: {bold: true}`
- **THEN** 表头应用 bold: true数据单元格使用 fonts_default 或系统默认字体
#### Scenario: 表格定义背景样式
- **WHEN** 表格定义 `style: {header_bg: "#4a90e2"}`
- **THEN** 系统将表头背景色设置为 #4a90e2
#### Scenario: 表格同时定义字体和背景样式
- **WHEN** 表格定义 `font: {size: 14}``style: {header_bg: "#4a90e2"}`
- **THEN** 系统同时应用字体样式和背景色
#### Scenario: 表格数据为空时报错

View File

@@ -0,0 +1,207 @@
# Font Extended
## Purpose
扩展字体属性在现有 size、bold、italic、color、align 基础上,新增 family、underline、strikethrough、line_spacing、space_before、space_after、baseline、caps 等属性,提供更完整的字体样式控制能力。
## Requirements
### Requirement: 元素 font 必须支持 family 属性
font 字段 SHALL 支持 family 属性,用于指定字体族名称。
#### Scenario: 设置字体族
- **WHEN** 元素定义 font: {family: "Arial"}
- **THEN** 系统将字体族设置为 Arial
#### Scenario: family 字段为 None
- **WHEN** 元素定义 font: {size: 18}(未定义 family
- **THEN** 系统使用继承或默认的字体族
### Requirement: 元素 font 必须支持 underline 属性
font 字段 SHALL 支持 underline 属性,控制文本是否带下划线。
#### Scenario: 启用下划线
- **WHEN** 元素定义 font: {underline: true}
- **THEN** 系统为文本添加下划线
#### Scenario: 禁用下划线
- **WHEN** 元素定义 font: {underline: false}
- **THEN** 系统不为文本添加下划线
#### Scenario: underline 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 underline
- **THEN** 系统使用继承或默认的下划线设置
### Requirement: 元素 font 必须支持 strikethrough 属性
font 字段 SHALL 支持 strikethrough 属性,控制文本是否带删除线。
#### Scenario: 启用删除线
- **WHEN** 元素定义 font: {strikethrough: true}
- **THEN** 系统为文本添加删除线
#### Scenario: 禁用删除线
- **WHEN** 元素定义 font: {strikethrough: false}
- **THEN** 系统不为文本添加删除线
#### Scenario: strikethrough 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 strikethrough
- **THEN** 系统使用继承或默认的删除线设置
### Requirement: 元素 font 必须支持 line_spacing 属性
font 字段 SHALL 支持 line_spacing 属性,控制行距倍数。
#### Scenario: 设置行距倍数
- **WHEN** 元素定义 font: {line_spacing: 1.5}
- **THEN** 系统将行距设置为 1.5 倍
#### Scenario: line_spacing 为 1.0
- **WHEN** 元素定义 font: {line_spacing: 1.0}
- **THEN** 系统使用单倍行距
#### Scenario: line_spacing 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 line_spacing
- **THEN** 系统使用继承或默认的行距设置
### Requirement: 元素 font 必须支持 space_before 属性
font 字段 SHALL 支持 space_before 属性,控制段前间距(单位:磅)。
#### Scenario: 设置段前间距
- **WHEN** 元素定义 font: {space_before: 12}
- **THEN** 系统将段前间距设置为 12 磅
#### Scenario: space_before 为 0
- **WHEN** 元素定义 font: {space_before: 0}
- **THEN** 系统不添加段前间距
#### Scenario: space_before 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 space_before
- **THEN** 系统使用继承或默认的段前间距
### Requirement: 元素 font 必须支持 space_after 属性
font 字段 SHALL 支持 space_after 属性,控制段后间距(单位:磅)。
#### Scenario: 设置段后间距
- **WHEN** 元素定义 font: {space_after: 12}
- **THEN** 系统将段后间距设置为 12 磅
#### Scenario: space_after 为 0
- **WHEN** 元素定义 font: {space_after: 0}
- **THEN** 系统不添加段后间距
#### Scenario: space_after 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 space_after
- **THEN** 系统使用继承或默认的段后间距
### Requirement: 元素 font 必须支持 baseline 属性
font 字段 SHALL 支持 baseline 属性控制文本基线位置normal、superscript、subscript
#### Scenario: 设置为上标
- **WHEN** 元素定义 font: {baseline: "superscript"}
- **THEN** 系统将文本设置为上标
#### Scenario: 设置为下标
- **WHEN** 元素定义 font: {baseline: "subscript"}
- **THEN** 系统将文本设置为下标
#### Scenario: 设置为正常基线
- **WHEN** 元素定义 font: {baseline: "normal"}
- **THEN** 系统使用正常基线位置
#### Scenario: baseline 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 baseline
- **THEN** 系统使用正常基线位置
#### Scenario: baseline 值无效
- **WHEN** 元素定义 font: {baseline: "invalid"}
- **THEN** 系统抛出 ERROR提示 baseline 必须是 normal、superscript 或 subscript
### Requirement: 元素 font 必须支持 caps 属性
font 字段 SHALL 支持 caps 属性控制文本大小写转换normal、allcaps、smallcaps
#### Scenario: 设置为全大写
- **WHEN** 元素定义 font: {caps: "allcaps"}
- **THEN** 系统将文本转换为大写
#### Scenario: 设置为小型大写
- **WHEN** 元素定义 font: {caps: "smallcaps"}
- **THEN** 系统将文本转换为小型大写字母
#### Scenario: 设置为正常大小写
- **WHEN** 元素定义 font: {caps: "normal"}
- **THEN** 系统保持文本原始大小写
#### Scenario: caps 未定义
- **WHEN** 元素定义 font: {size: 18}(未定义 caps
- **THEN** 系统保持文本原始大小写
#### Scenario: caps 值无效
- **WHEN** 元素定义 font: {caps: "invalid"}
- **THEN** 系统抛出 ERROR提示 caps 必须是 normal、allcaps 或 smallcaps
### Requirement: 多行文本必须将所有属性应用到每个段落
当文本内容包含换行符时,系统 SHALL 将所有字体属性(包括扩展属性)应用到文本框中的每个段落。
#### Scenario: 多行文本应用扩展属性
- **WHEN** 文本内容包含换行符且定义 font: {size: 12, underline: true, line_spacing: 1.5}
- **THEN** 系统将所有属性size、underline、line_spacing应用到所有段落
#### Scenario: 多行文本每个段落样式一致
- **WHEN** 文本包含多个换行符且定义了 font 属性
- **THEN** 每个段落的字体样式都应一致
### Requirement: 扩展属性必须支持继承机制
扩展属性 SHALL 遵循与基础属性相同的继承机制parent → 当前定义 → fonts_default → 系统默认。
#### Scenario: 扩展属性从 parent 继承
- **WHEN** parent 定义 underline: true当前定义未指定 underline
- **THEN** 元素使用 underline: true从 parent 继承)
#### Scenario: 扩展属性从 fonts_default 继承
- **WHEN** fonts_default 定义 line_spacing: 1.5,元素未指定 line_spacing
- **THEN** 元素使用 line_spacing: 1.5(从 fonts_default 继承)
#### Scenario: 当前定义覆盖继承的扩展属性
- **WHEN** parent 定义 space_before: 12当前定义 space_before: 24
- **THEN** 元素使用 space_before: 24当前定义覆盖

View File

@@ -0,0 +1,117 @@
# Font Preset
## Purpose
预设字体类别提供系统内置的字体类别名称,用户可以直接使用这些类别名称引用推荐的字体,无需指定具体字体名称。系统将预设类别映射到跨平台通用的推荐字体。
## Requirements
### Requirement: 系统必须支持五种预设字体类别
系统 SHALL 支持以下预设字体类别sans、serif、mono、cjk-sans、cjk-serif。
#### Scenario: 使用 sans 类别
- **WHEN** 元素定义 font: {family: "sans"}
- **THEN** 系统使用 "Arial" 作为字体族
#### Scenario: 使用 serif 类别
- **WHEN** 元素定义 font: {family: "serif"}
- **THEN** 系统使用 "Times New Roman" 作为字体族
#### Scenario: 使用 mono 类别
- **WHEN** 元素定义 font: {family: "mono"}
- **THEN** 系统使用 "Courier New" 作为字体族
#### Scenario: 使用 cjk-sans 类别
- **WHEN** 元素定义 font: {family: "cjk-sans"}
- **THEN** 系统使用 "Microsoft YaHei" 作为字体族
#### Scenario: 使用 cjk-serif 类别
- **WHEN** 元素定义 font: {family: "cjk-serif"}
- **THEN** 系统使用 "SimSun" 作为字体族
### Requirement: 预设类别映射必须使用跨平台通用字体
预设字体类别 SHALL 映射到跨平台通用性最高的字体,确保在不同操作系统上都有较好的显示效果。
#### Scenario: sans 类别映射到 Arial
- **WHEN** 系统解析 family: "sans"
- **THEN** 映射到 "Arial"Windows/macOS/Linux 通用)
#### Scenario: serif 类别映射到 Times New Roman
- **WHEN** 系统解析 family: "serif"
- **THEN** 映射到 "Times New Roman"Windows/macOS/Linux 通用)
#### Scenario: mono 类别映射到 Courier New
- **WHEN** 系统解析 family: "mono"
- **THEN** 映射到 "Courier New"Windows/macOS/Linux 通用)
#### Scenario: cjk-sans 类别映射到微软雅黑
- **WHEN** 系统解析 family: "cjk-sans"
- **THEN** 映射到 "Microsoft YaHei"Windows 常用中文字体)
#### Scenario: cjk-serif 类别映射到宋体
- **WHEN** 系统解析 family: "cjk-serif"
- **THEN** 映射到 "SimSun"Windows 常用中文字体)
### Requirement: 预设类别必须在 family 字段中识别
系统 SHALL 仅在 font 或 header_font 的 family 字段中识别预设类别名称。
#### Scenario: family 字段使用预设类别
- **WHEN** 元素定义 font: {family: "sans"}
- **THEN** 系统识别 "sans" 为预设类别,映射到 "Arial"
#### Scenario: 其他字段不解析预设类别
- **WHEN** 元素定义 font: {size: "sans"}
- **THEN** 系统将 "sans" 作为字符串值,不进行预设类别映射
#### Scenario: 直接使用字体名称
- **WHEN** 元素定义 font: {family: "SimSun"}
- **THEN** 系统使用 "SimSun" 作为字体族,不进行预设类别映射
### Requirement: 预设类别不进行字体验证
系统 SHALL 不验证预设类别映射的字体是否在系统中存在。
#### Scenario: 预设类别字体不存在时行为
- **WHEN** 系统中不存在 Arial 字体
- **THEN** 系统仍将 "sans" 映射到 "Arial",不报错
#### Scenario: PowerPoint 处理字体回退
- **WHEN** PowerPoint 打开包含不存在字体的 PPTX 文件
- **THEN** PowerPoint 自动回退到系统默认字体
### Requirement: 预设类别可以与 fonts 配置结合使用
用户可以在 fonts 配置中使用预设类别,也可以在元素 font 中直接使用。
#### Scenario: fonts 配置中使用预设类别
- **WHEN** metadata 定义 fonts: {body: {family: "sans", size: 18}}
- **THEN** 系统将 family: "sans" 解析为 "Arial"
#### Scenario: 元素直接使用预设类别
- **WHEN** 元素定义 font: {family: "cjk-sans", size: 24}
- **THEN** 系统将 family: "cjk-sans" 解析为 "Microsoft YaHei"
#### Scenario: 预设类别与引用结合
- **WHEN** fonts.title 定义 family: "sans",元素定义 font: "@title"
- **THEN** 元素使用 family: "Arial"(通过 title 配置)

View File

@@ -0,0 +1,207 @@
# Font Theme
## Purpose
字体主题系统提供可复用的字体配置管理能力,允许用户在 metadata 中定义字体配置模板,通过引用方式应用到元素,实现统一的字体样式管理。
## Requirements
### Requirement: 系统必须支持在 metadata 中定义 fonts 字段
系统 SHALL 支持在 YAML metadata 中定义 fonts 字段,用于存储可复用的字体配置。
#### Scenario: 定义 fonts 字段
- **WHEN** metadata 中定义 fonts 字段
- **THEN** 系统成功解析并存储字体配置字典
#### Scenario: fonts 字段为空字典
- **WHEN** metadata 中定义 fonts: {}
- **THEN** 系统接受空的字体配置字典
#### Scenario: fonts 字段未定义
- **WHEN** metadata 中未定义 fonts 字段
- **THEN** 系统正常处理fonts 为空字典
### Requirement: fonts 字段必须包含命名字体配置
fonts 字段 SHALL 包含一个或多个命名字体配置,每个配置是一个字体属性字典。
#### Scenario: 定义单个字体配置
- **WHEN** fonts 中定义 title: {family: "Arial", size: 44, bold: true}
- **THEN** 系统创建名为 title 的字体配置
#### Scenario: 定义多个字体配置
- **WHEN** fonts 中定义 title、subtitle、body 等多个配置
- **THEN** 系统为每个配置创建独立的字体对象
### Requirement: 系统必须支持 fonts_default 字段
系统 SHALL 支持在 metadata 中定义可选的 fonts_default 字段,指定默认字体配置的引用。
#### Scenario: 定义 fonts_default 引用
- **WHEN** metadata 中定义 fonts_default: "@body"
- **THEN** 系统将 fonts_default 解析为对 fonts.body 的引用
#### Scenario: fonts_default 未定义
- **WHEN** metadata 中未定义 fonts_default 字段
- **THEN** 系统使用 python-pptx 的默认字体
#### Scenario: fonts_default 引用不存在的配置
- **WHEN** fonts_default: "@undefined" 且 fonts.undefined 不存在
- **THEN** 系统抛出 ERROR提示引用的字体配置不存在
#### Scenario: fonts_default 必须引用 fonts 中的配置
- **WHEN** fonts_default: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR提示 fonts_default 必须是引用格式
### Requirement: 元素 font 字段必须支持三种引用方式
元素 font 字段 SHALL 支持字符串引用(整体引用)、字典引用(继承覆盖或独立定义)两种形式。
#### Scenario: 字符串整体引用
- **WHEN** 元素定义 font: "@title"
- **THEN** 系统使用 fonts.title 的所有属性
#### Scenario: 字典继承覆盖
- **WHEN** 元素定义 font: {parent: "@title", size: 60}
- **THEN** 系统继承 fonts.title 的所有属性,覆盖 size 为 60
#### Scenario: 字典独立定义
- **WHEN** 元素定义 font: {family: "SimSun", size: 24}
- **THEN** 系统使用定义的属性,未定义的属性继承 fonts_default
#### Scenario: font 字段未定义
- **WHEN** 元素未定义 font 字段
- **THEN** 元素使用 fonts_default 或系统默认字体
### Requirement: parent 字段必须引用 fonts 中的配置
font 字典中的 parent 字段 SHALL 引用 fonts 中已定义的配置名称。
#### Scenario: parent 引用存在的配置
- **WHEN** parent: "@title" 且 fonts.title 存在
- **THEN** 系统成功继承 fonts.title 的属性
#### Scenario: parent 引用不存在的配置
- **WHEN** parent: "@undefined" 且 fonts.undefined 不存在
- **THEN** 系统抛出 ERROR提示引用的字体配置不存在
#### Scenario: parent 必须是引用格式
- **WHEN** parent: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR提示 parent 必须是引用格式
### Requirement: 系统必须检测并拒绝引用循环
系统 SHALL 在解析字体引用时检测循环引用,检测到循环时抛出 ERROR。
#### Scenario: 直接循环引用
- **WHEN** fonts.title.parent: "@title"(引用自身)
- **THEN** 系统抛出 ERROR提示检测到自引用
#### Scenario: 间接循环引用
- **WHEN** fonts.a.parent: "@b" 且 fonts.b.parent: "@a"
- **THEN** 系统抛出 ERROR显示完整的引用循环路径
#### Scenario: 深层循环引用
- **WHEN** 引用链深度超过 10 层
- **THEN** 系统抛出 ERROR提示引用深度超限
#### Scenario: 错误信息包含引用路径
- **WHEN** 系统检测到循环引用
- **THEN** 错误信息包含完整的引用路径,如 "fonts.title -> fonts.subtitle -> fonts.title"
### Requirement: 系统必须支持属性继承链
字体属性解析 SHALL 按照优先级顺序继承parent → 当前定义 → fonts_default → 系统默认。
#### Scenario: parent 定义了基础属性
- **WHEN** fonts.title 定义 size: 44元素定义 font: {parent: "@title", bold: true}
- **THEN** 元素使用 size: 44继承、bold: true覆盖
#### Scenario: parent 未定义的属性继承 fonts_default
- **WHEN** fonts_default 定义 size: 18元素定义 font: {parent: "@title"} 且 title 未定义 size
- **THEN** 元素使用 size: 18从 fonts_default 继承)
#### Scenario: 当前定义覆盖 parent
- **WHEN** parent 定义 size: 44当前定义 size: 60
- **THEN** 元素使用 size: 60当前定义覆盖 parent
### Requirement: 模板元素必须支持继承 fonts_default
模板中未定义 font 的元素 SHALL 继承 metadata.fonts_default 配置。
#### Scenario: 模板元素未定义 font
- **WHEN** 模板元素未定义 font 字段
- **THEN** 元素继承 metadata.fonts_default 的配置
#### Scenario: 模板元素定义了 font
- **WHEN** 模板元素定义 font: "@title"
- **THEN** 元素使用 font: "@title",不继承 fonts_default
#### Scenario: fonts_default 未定义时模板元素行为
- **WHEN** 模板元素未定义 font 且 metadata 未定义 fonts_default
- **THEN** 元素使用系统默认字体
### Requirement: 表格元素必须支持 font 和 header_font 字段
表格元素 SHALL 支持 font 和 header_font 字段,分别控制数据单元格和表头的字体样式。
#### Scenario: 表格定义整体字体
- **WHEN** 表格定义 font: {family: "Arial", size: 14}
- **THEN** 数据单元格和表头都应用该字体(表头可被 header_font 覆盖)
#### Scenario: 表格定义表头字体
- **WHEN** 表格定义 header_font: {bold: true, color: "#ffffff"}
- **THEN** 表头应用该字体,数据单元格继承 font 或 fonts_default
#### Scenario: 表头字体继承表格字体
- **WHEN** 表格定义 font: {size: 14} 且 header_font: {parent: "@font", bold: true}
- **THEN** 表头继承 size: 14覆盖 bold: true
#### Scenario: 表格仅定义 header_font
- **WHEN** 表格仅定义 header_font: {bold: true}
- **THEN** 表头应用 bold: true数据单元格继承 fonts_default
### Requirement: 系统必须移除旧的表格字体语法
系统 SHALL 移除 style.font_size 和 style.header_color 字段的处理逻辑。
#### Scenario: 旧语法字段不再生效
- **WHEN** 表格定义 style: {font_size: 14, header_color: "#fff"}
- **THEN** 系统忽略这些字段,使用 font 和 header_font 替代
#### Scenario: style 字段保留用于非字体属性
- **WHEN** 表格定义 style: {header_bg: "#4a90e2"}
- **THEN** 系统正常处理背景色等非字体属性