""" PPTX 渲染器模块 封装 python-pptx 操作,将元素对象渲染为 PPTX 幻灯片。 """ from pathlib import Path from pptx import Presentation as PptxPresentation from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN from pptx.enum.shapes import MSO_SHAPE from pptx.dml.color import RGBColor from core.elements import TextElement, ImageElement, ShapeElement, TableElement from loaders.yaml_loader import YAMLError from utils import hex_to_rgb class PptxGenerator: """PPTX 生成器,封装 python-pptx 操作""" def __init__(self, size='16:9'): """ 初始化 PPTX 生成器 Args: size: 幻灯片尺寸("16:9" 或 "4:3") """ self.prs = PptxPresentation() # 设置幻灯片尺寸 if size == '16:9': self.prs.slide_width = Inches(10) self.prs.slide_height = Inches(5.625) elif size == '4:3': self.prs.slide_width = Inches(10) self.prs.slide_height = Inches(7.5) else: raise YAMLError(f"不支持的尺寸比例: {size},仅支持 16:9 和 4:3") def add_slide(self, slide_data, base_path=None): """ 添加幻灯片并渲染所有元素 Args: slide_data: 包含 background 和 elements 的字典 base_path: 基础路径(用于相对路径解析) """ # 使用空白布局(layout[6]) blank_layout = self.prs.slide_layouts[6] slide = self.prs.slides.add_slide(blank_layout) # 设置背景 background = slide_data.get('background') if background: self._render_background(slide, background, base_path) # 按顺序渲染所有元素 elements = slide_data.get('elements', []) for elem in elements: self._render_element(slide, elem, base_path) def _render_element(self, slide, elem, base_path): """ 分发元素到对应的渲染方法 Args: slide: pptx slide 对象 elem: 元素对象 base_path: 基础路径 """ if isinstance(elem, TextElement): self._render_text(slide, elem) elif isinstance(elem, ImageElement): self._render_image(slide, elem, base_path) elif isinstance(elem, ShapeElement): self._render_shape(slide, elem) elif isinstance(elem, TableElement): self._render_table(slide, elem) def _render_text(self, slide, elem: TextElement): """ 渲染文本元素 Args: slide: pptx slide 对象 elem: TextElement 对象 """ # 获取位置和尺寸 x, y, w, h = [Inches(v) for v in elem.box] # 创建文本框 textbox = slide.shapes.add_textbox(x, y, w, h) tf = textbox.text_frame tf.text = elem.content # 默认启用文字自动换行 tf.word_wrap = True # 应用字体样式 p = tf.paragraphs[0] # 字体大小 if 'size' in elem.font: p.font.size = Pt(elem.font['size']) # 粗体 if elem.font.get('bold'): p.font.bold = True # 斜体 if elem.font.get('italic'): p.font.italic = True # 颜色 if 'color' in elem.font: rgb = hex_to_rgb(elem.font['color']) p.font.color.rgb = RGBColor(*rgb) # 对齐方式 align_map = { 'left': PP_ALIGN.LEFT, 'center': PP_ALIGN.CENTER, 'right': PP_ALIGN.RIGHT } align = elem.font.get('align', 'left') p.alignment = align_map.get(align, PP_ALIGN.LEFT) def _render_image(self, slide, elem: ImageElement, base_path): """ 渲染图片元素 Args: slide: pptx slide 对象 elem: ImageElement 对象 base_path: 基础路径 """ # 获取图片路径 img_path = Path(elem.src) # 处理相对路径 if not img_path.is_absolute() and base_path: img_path = Path(base_path) / elem.src # 检查文件是否存在 if not img_path.exists(): raise YAMLError(f"图片文件未找到: {img_path}") # 获取位置和尺寸 x, y, w, h = [Inches(v) for v in elem.box] # 添加图片 try: slide.shapes.add_picture(str(img_path), x, y, width=w, height=h) except Exception as e: raise YAMLError(f"添加图片失败: {img_path}: {str(e)}") def _render_shape(self, slide, elem: ShapeElement): """ 渲染形状元素 Args: slide: pptx slide 对象 elem: ShapeElement 对象 """ # 获取形状类型 shape_type_map = { 'rectangle': MSO_SHAPE.RECTANGLE, 'ellipse': MSO_SHAPE.OVAL, 'rounded_rectangle': MSO_SHAPE.ROUNDED_RECTANGLE, } mso_shape = shape_type_map.get(elem.shape, MSO_SHAPE.RECTANGLE) # 获取位置和尺寸 x, y, w, h = [Inches(v) for v in elem.box] # 添加形状 shape = slide.shapes.add_shape(mso_shape, x, y, w, h) # 应用填充色 if elem.fill: rgb = hex_to_rgb(elem.fill) shape.fill.solid() shape.fill.fore_color.rgb = RGBColor(*rgb) # 应用边框样式 if elem.line: if 'color' in elem.line: rgb = hex_to_rgb(elem.line['color']) shape.line.color.rgb = RGBColor(*rgb) if 'width' in elem.line: shape.line.width = Pt(elem.line['width']) def _render_table(self, slide, elem: TableElement): """ 渲染表格元素 Args: slide: pptx slide 对象 elem: TableElement 对象 """ # 获取表格数据 rows = len(elem.data) cols = len(elem.data[0]) if elem.data else 0 # 获取列宽 col_widths = elem.col_widths if elem.col_widths else [2] * cols if len(col_widths) != cols: raise YAMLError(f"列宽数量({len(col_widths)})与列数({cols})不匹配") # 获取位置 x, y = [Inches(v) for v in elem.position] # 计算总宽度和高度 total_width = Inches(sum(col_widths)) row_height = Inches(0.5) # 创建表格 table = slide.shapes.add_table(rows, cols, x, y, total_width, row_height * rows).table # 设置列宽 for i, width in enumerate(col_widths): table.columns[i].width = Inches(width) # 填充数据 for i, row_data in enumerate(elem.data): for j, cell_value in enumerate(row_data): cell = table.cell(i, j) cell.text = str(cell_value) # 应用样式 # 字体大小 if 'font_size' in elem.style: for row in table.rows: for cell in row.cells: cell.text_frame.paragraphs[0].font.size = Pt(elem.style['font_size']) # 表头样式 if 'header_bg' in elem.style or 'header_color' in elem.style: for i, cell in enumerate(table.rows[0].cells): if 'header_bg' in elem.style: rgb = hex_to_rgb(elem.style['header_bg']) cell.fill.solid() cell.fill.fore_color.rgb = RGBColor(*rgb) if 'header_color' in elem.style: rgb = hex_to_rgb(elem.style['header_color']) cell.text_frame.paragraphs[0].font.color.rgb = RGBColor(*rgb) def _render_background(self, slide, background, base_path=None): """ 设置幻灯片背景 Args: slide: pptx slide 对象 background: 背景字典,包含 color 或 image base_path: 基础路径 """ if not background: return # 纯色背景 if 'color' in background: bg = slide.background fill = bg.fill fill.solid() rgb = hex_to_rgb(background['color']) fill.fore_color.rgb = RGBColor(*rgb) # 图片背景(可选功能,简单实现) elif 'image' in background: # 图片背景需要更复杂的处理,暂时跳过 from utils import log_info log_info(f"图片背景暂未实现: {background['image']}") def save(self, output_path): """ 保存 PPTX 文件 Args: output_path: 输出文件路径 """ output_path = Path(output_path) # 自动创建输出目录 output_path.parent.mkdir(parents=True, exist_ok=True) # 保存文件 try: self.prs.save(str(output_path)) except PermissionError: raise YAMLError(f"权限不足,无法写入文件: {output_path}") except Exception as e: raise YAMLError(f"保存文件失败: {output_path}: {str(e)}")