""" 演示文稿模块 管理整个演示文稿的生成流程。 """ 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"), # 保留幻灯片描述 }