from typing import Dict, Any, List, Optional, Callable from pptx import Presentation from loguru import logger import ast import operator class ConditionalRenderer: def __init__(self, presentation: Presentation = None): self.prs = presentation self.context = {} self._slides_to_remove = [] self.operators = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Gt: operator.gt, ast.GtE: operator.ge, ast.Lt: operator.lt, ast.LtE: operator.le, ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.And: lambda a, b: a and b, ast.Or: lambda a, b: a or b, } def set_context(self, context: Dict[str, Any]): self.context = context logger.info(f"条件渲染上下文已设置: {list(context.keys())}") def update_context(self, key: str, value: Any): self.context[key] = value def evaluate_condition(self, condition_expr: str, context: Dict[str, Any] = None) -> bool: eval_context = {**self.context, **(context or {})} try: if "{" in condition_expr: for key, value in eval_context.items(): condition_expr = condition_expr.replace(f"{{{key}}}", str(value)) result = self._safe_eval(condition_expr, eval_context) logger.info(f"条件 [{condition_expr}] 评估结果: {result}") return bool(result) except Exception as e: logger.warning(f"条件表达式评估失败 [{condition_expr}]: {e}") return False def _safe_eval(self, expr: str, context: Dict) -> Any: try: node = ast.parse(expr, mode='eval') return self._eval_ast(node.body, context) except: safe_dict = {k: v for k, v in context.items() if isinstance(k, str)} safe_dict['__builtins__'] = {} return eval(expr, safe_dict) def _eval_ast(self, node, context): if isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.Num): return node.n elif isinstance(node, ast.Str): return node.s elif isinstance(node, ast.Name): return context.get(node.id, node.id) elif isinstance(node, ast.BinOp): op_type = type(node.op) if op_type in self.operators: return self.operators[op_type]( self._eval_ast(node.left, context), self._eval_ast(node.right, context) ) elif isinstance(node, ast.Compare): left_val = self._eval_ast(node.left, context) for op, comparator in zip(node.ops, node.comparators): op_type = type(op) if op_type in self.operators: if not self.operators[op_type](left_val, self._eval_ast(comparator, context)): return False return True elif isinstance(node, ast.BoolOp): op_type = type(node.op) if op_type == ast.And: for value in node.values: if not self._eval_ast(value, context): return False return True elif op_type == ast.Or: for value in node.values: if self._eval_ast(value, context): return True return False return None def process_slide_conditions(self, slide_configs: List[Dict], presentation: Presentation = None) -> Presentation: prs = presentation or self.prs if not prs: logger.error("没有提供Presentation对象") return prs logger.info(f"开始处理条件渲染,当前页数: {len(prs.slides)}") slides_to_keep = [] for idx, slide_config in enumerate(slide_configs): if 'condition' in slide_config: condition = slide_config['condition'] if not self.evaluate_condition(condition): logger.info(f"跳过第 {idx+1} 页,条件不满足: {condition}") continue if 'action' in slide_config and slide_config['action'] == 'insert_slide': logger.info(f"执行插入幻灯片操作: {slide_config.get('template_source')}") if idx < len(prs.slides): slides_to_keep.append(idx) logger.info(f"条件渲染完成,保留 {len(slides_to_keep)} / {len(slide_configs)} 页配置") return prs def insert_new_slide(self, presentation: Presentation, slide_layout_idx: int = 6, position: int = None) -> int: if position is None: position = len(presentation.slides) slide_layout = presentation.slide_layouts[slide_layout_idx] if slide_layout_idx < len(presentation.slide_layouts) else presentation.slide_layouts[0] xml_slides = presentation.slides._sldIdLst slides = list(xml_slides) xml_slides.insert(position, slides[0] if slides else None) new_slide = presentation.slides.add_slide(slide_layout) logger.info(f"已在位置 {position} 插入新页面") return position