""" 演示文稿模块 管理整个演示文稿的生成流程。 """ 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 from utils.font_resolver import FontResolver 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 # 获取演示文稿尺寸和描述 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.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)) # size 一致性校验 template_metadata = self.template_library.get("metadata", {}) template_size = template_metadata.get("size") if self.size != template_size: raise YAMLError( f"文档尺寸 '{self.size}' 与模板库尺寸 '{template_size}' 不一致" ) # 解析字体配置 self.fonts = metadata.get("fonts", {}) self.fonts_default = metadata.get("fonts_default") # 解析模板库字体配置 self.template_fonts = {} self.template_fonts_default = None if self.template_library: template_metadata = self.template_library.get("metadata", {}) self.template_fonts = template_metadata.get("fonts", {}) self.template_fonts_default = template_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 或 template_fonts 中 font_name = self.fonts_default[1:] if font_name not in self.fonts and font_name not in self.template_fonts: raise YAMLError( f"fonts_default 引用的字体配置不存在: {self.fonts_default}" ) # 创建字体解析器 # 文档作用域的 FontResolver(可引用模板库字体) self.font_resolver_doc = FontResolver( fonts=self.fonts, fonts_default=self.fonts_default, scope="document", template_fonts=self.template_fonts ) # 模板库作用域的 FontResolver(只能引用模板库字体) self.font_resolver_template = FontResolver( fonts=self.template_fonts, fonts_default=self.template_fonts_default, scope="template", template_fonts=self.template_fonts ) # 解析并保存内联模板 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,文档作用域,文档 FontResolver return Template.from_data( inline_data, template_name, base_dir=self.pres_base_dir, scope="document", font_resolver=self.font_resolver_doc ) # 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,模板作用域,模板库 FontResolver return Template.from_data( template_data, template_name, base_dir=self.template_base_dir, scope="template", font_resolver=self.font_resolver_template ) 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 # 解析自定义元素的字体引用(使用文档 FontResolver) for elem in elements_from_custom: if isinstance(elem, dict): # 处理普通元素的 font 字段 if 'font' in elem: font_config = elem['font'] if isinstance(font_config, (str, dict)): try: resolved_font = self.font_resolver_doc.resolve_font(font_config) elem['font'] = resolved_font except ValueError as e: raise YAMLError(f"字体解析失败: {str(e)}") elif elem.get('type') in ['text', 'table']: # 元素未定义 font,使用 fonts_default(如果有) if self.fonts_default: try: resolved_font = self.font_resolver_doc.resolve_font(None) elem['font'] = resolved_font except ValueError as e: raise YAMLError(f"字体解析失败: {str(e)}") # 处理表格元素的 header_font 字段 if elem.get('type') == 'table' and 'header_font' in elem: header_font_config = elem['header_font'] if isinstance(header_font_config, (str, dict)): try: resolved_header_font = self.font_resolver_doc.resolve_font(header_font_config) elem['header_font'] = resolved_header_font except ValueError as e: raise YAMLError(f"表格 header_font 解析失败: {str(e)}") # 解析自定义元素的图片路径(相对于文档目录) 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"), # 保留幻灯片描述 }