1
0
Files
PPTX/core/presentation.py
lanyuanxiaoyao f1aae96a04 refactor: 重构外部模板系统,改为单文件模板库模式
主要变更:
- 将 templates_dir 参数改为 template_file,支持单个模板库 YAML 文件
- 添加模板库 YAML 验证功能
- 为模板添加 base_dir 支持,正确解析相对路径资源
- 内联模板与外部模板同名时改为警告(内联优先)
- 移除模板缓存机制,直接使用模板库字典
- 更新所有相关测试以适配新的模板加载方式

此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
2026-03-05 13:27:12 +08:00

187 lines
7.3 KiB
Python
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.
"""
演示文稿模块
管理整个演示文稿的生成流程。
"""
from pathlib import Path
from loaders.yaml_loader import load_yaml_file, validate_presentation_yaml, validate_template_library_yaml, YAMLError
from core.template import Template
from core.elements import create_element
class Presentation:
"""演示文稿类,管理整个演示文稿的生成流程"""
def __init__(self, pres_file, template_file=None):
"""
初始化演示文稿
Args:
pres_file: 演示文稿 YAML 文件路径
template_file: 模板库文件路径(可选)
"""
self.pres_file = Path(pres_file).resolve()
self.template_file = Path(template_file).resolve() if template_file else None
# 加载演示文稿文件
self.data = load_yaml_file(pres_file)
validate_presentation_yaml(self.data, str(pres_file))
# 保存文档目录和模板库目录(使用绝对路径)
self.pres_base_dir = self.pres_file.parent
self.template_base_dir = self.template_file.parent if self.template_file else None
# 加载并验证模板库文件(如果提供)
self.template_library = None
if self.template_file:
if not self.template_file.exists():
raise YAMLError(f"模板库文件不存在: {self.template_file}")
self.template_library = load_yaml_file(self.template_file)
validate_template_library_yaml(self.template_library, str(self.template_file))
# 获取演示文稿尺寸
metadata = self.data.get("metadata", {})
self.size = metadata.get("size", "16:9")
self.description = metadata.get("description") # 可选的描述字段
# 验证尺寸值
if not isinstance(self.size, str):
raise ValueError(
f"无效的尺寸值: {self.size},尺寸必须是字符串(如 '16:9''4:3'"
)
# 解析字体配置
self.fonts = metadata.get("fonts", {})
self.fonts_default = metadata.get("fonts_default")
# 验证 fonts_default
if self.fonts_default:
# fonts_default 必须是引用格式
if not isinstance(self.fonts_default, str) or not self.fonts_default.startswith("@"):
raise YAMLError(
f"fonts_default 必须是引用格式(@xxx当前值: {self.fonts_default}"
)
# fonts_default 引用的配置必须存在于 fonts 中
font_name = self.fonts_default[1:]
if font_name not in self.fonts:
raise YAMLError(
f"fonts_default 引用的字体配置不存在: {self.fonts_default}"
)
# 解析并保存内联模板
self.inline_templates = self.data.get('templates', {})
def get_template(self, template_name):
"""
获取模板(优先检查内联模板,检测同名冲突)
Args:
template_name: 模板名称
Returns:
Template 对象
Raises:
YAMLError: 模板不存在
"""
# 1. 先检查内联模板
if template_name in self.inline_templates:
# 2. 检查外部模板是否也存在同名WARNING
if self._external_template_exists(template_name):
# 同名冲突:发出警告但继续使用内联模板
# 注意:这里只是记录警告,实际的 WARNING 级别验证问题应该在验证器中生成
pass
inline_data = self.inline_templates[template_name]
# 内联模板使用文档目录作为 base_dir
return Template.from_data(inline_data, template_name, base_dir=self.pres_base_dir)
# 3. 回退到外部模板
if not self.template_library:
raise YAMLError(
f"模板不存在: {template_name}\n"
f"提示: 该模板既不在内联模板中,也未提供外部模板库文件"
)
# 从模板库字典查找
if template_name not in self.template_library['templates']:
raise YAMLError(
f"模板库中找不到指定模板名称: {template_name}\n"
f"模板库文件: {self.template_file}"
)
template_data = self.template_library['templates'][template_name]
# 外部模板使用模板库目录作为 base_dir
return Template.from_data(template_data, template_name, base_dir=self.template_base_dir)
def _external_template_exists(self, template_name):
"""检查外部模板是否存在"""
if not self.template_library:
return False
return template_name in self.template_library['templates']
def render_slide(self, slide_data):
"""
渲染单个幻灯片(支持混合模式)
Args:
slide_data: 幻灯片数据字典
Returns:
dict: 包含 background 和 elements 的字典
支持三种模式:
1. 纯模板模式:只有 template 字段
2. 纯自定义模式:只有 elements 字段
3. 混合模式:同时有 template 和 elements 字段
"""
has_template = "template" in slide_data
has_custom_elements = slide_data.get("elements")
elements_from_template = []
vars_values = {}
# 步骤1渲染模板如果有
if has_template:
template_name = slide_data["template"]
template = self.get_template(template_name)
vars_values = slide_data.get("vars", {})
elements_from_template = template.render(vars_values)
# 步骤2处理自定义元素如果有
elements_from_custom = []
if has_custom_elements:
custom_elements = slide_data["elements"]
if has_template:
# 混合模式:使用模板变量解析自定义元素
template = self.get_template(slide_data["template"])
elements_from_custom = [
template.resolve_element(elem, vars_values)
for elem in custom_elements
]
else:
# 纯自定义模式(原有行为)
elements_from_custom = custom_elements
# 解析自定义元素的图片路径(相对于文档目录)
for elem in elements_from_custom:
if isinstance(elem, dict) and elem.get('type') == 'image':
src = elem.get('src')
if src:
src_path = Path(src)
# 只处理相对路径
if not src_path.is_absolute():
resolved_path = self.pres_base_dir / src
elem['src'] = str(resolved_path)
# 步骤3合并元素模板元素在前自定义元素在后
final_elements = elements_from_template + elements_from_custom
# 步骤4转换为元素对象
element_objects = [create_element(elem) for elem in final_elements]
return {
"background": slide_data.get("background"),
"elements": element_objects,
"description": slide_data.get("description"), # 保留幻灯片描述
}