""" 元素抽象层模块 定义统一的元素数据类,支持元素验证和未来扩展。 """ from dataclasses import dataclass, field from typing import Optional, List, Union import re @dataclass class FontConfig: """字体配置数据类""" parent: Optional[str] = None family: Optional[str] = None size: Optional[int] = None bold: Optional[bool] = None italic: Optional[bool] = None underline: Optional[bool] = None strikethrough: Optional[bool] = None color: Optional[str] = None align: Optional[str] = None line_spacing: Optional[float] = None space_before: Optional[int] = None space_after: Optional[int] = None baseline: Optional[str] = None caps: Optional[str] = None def __post_init__(self): """验证枚举值""" if self.baseline is not None and self.baseline not in ['normal', 'superscript', 'subscript']: raise ValueError(f"baseline 必须是 normal、superscript 或 subscript,当前值: {self.baseline}") if self.caps is not None and self.caps not in ['normal', 'allcaps', 'smallcaps']: raise ValueError(f"caps 必须是 normal、allcaps 或 smallcaps,当前值: {self.caps}") def _is_valid_color(color: str) -> bool: """ 验证颜色格式是否正确 Args: color: 颜色字符串 Returns: 是否为有效的颜色格式(#RGB 或 #RRGGBB) """ pattern = r'^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$' return bool(re.match(pattern, color)) @dataclass class TextElement: """文本元素""" type: str = 'text' content: str = '' box: list = field(default_factory=lambda: [1, 1, 8, 1]) font: Union[FontConfig, str, dict] = field(default_factory=dict) def __post_init__(self): """创建时验证""" if not isinstance(self.box, list) or len(self.box) != 4: raise ValueError("box 必须是包含 4 个数字的列表") def validate(self) -> List: """验证元素自身的完整性""" from validators.result import ValidationIssue issues = [] # 只在 font 是字典类型时进行验证 # 字符串引用和 FontConfig 对象会在渲染时由 FontResolver 处理 if isinstance(self.font, dict): # 检查颜色格式 if self.font.get('color'): if not _is_valid_color(self.font['color']): issues.append(ValidationIssue( level="ERROR", message=f"无效的颜色格式: {self.font['color']} (应为 #RRGGBB)", location="", code="INVALID_COLOR_FORMAT" )) # 检查字体大小 if self.font.get('size'): size = self.font['size'] if size < 8: issues.append(ValidationIssue( level="WARNING", message=f"字体太小: {size}pt (建议 >= 8pt)", location="", code="FONT_TOO_SMALL" )) elif size > 100: issues.append(ValidationIssue( level="WARNING", message=f"字体太大: {size}pt (建议 <= 100pt)", location="", code="FONT_TOO_LARGE" )) return issues @dataclass class ImageElement: """图片元素""" type: str = 'image' src: str = '' box: list = field(default_factory=lambda: [1, 1, 4, 3]) def __post_init__(self): """创建时验证""" if not self.src: raise ValueError("图片元素必须指定 src") if not self.box: raise ValueError("图片元素必须指定 box") if not isinstance(self.box, list) or len(self.box) != 4: raise ValueError("box 必须是包含 4 个数字的列表") def validate(self) -> List: """验证元素自身的完整性""" return [] @dataclass class ShapeElement: """形状元素""" type: str = 'shape' shape: str = 'rectangle' box: list = field(default_factory=lambda: [1, 1, 2, 1]) fill: Optional[str] = None line: Optional[dict] = None def __post_init__(self): """创建时验证""" if not isinstance(self.box, list) or len(self.box) != 4: raise ValueError("box 必须是包含 4 个数字的列表") def validate(self) -> List: """验证元素自身的完整性""" from validators.result import ValidationIssue issues = [] # 检查形状类型枚举 valid_shapes = ['rectangle', 'ellipse', 'rounded_rectangle'] if self.shape not in valid_shapes: issues.append(ValidationIssue( level="ERROR", message=f"不支持的形状类型: {self.shape} (支持: {', '.join(valid_shapes)})", location="", code="INVALID_SHAPE_TYPE" )) # 检查填充颜色格式 if self.fill and not _is_valid_color(self.fill): issues.append(ValidationIssue( level="ERROR", message=f"无效的填充颜色格式: {self.fill} (应为 #RRGGBB)", location="", code="INVALID_COLOR_FORMAT" )) # 检查线条颜色格式 if self.line and self.line.get('color'): if not _is_valid_color(self.line['color']): issues.append(ValidationIssue( level="ERROR", message=f"无效的线条颜色格式: {self.line['color']} (应为 #RRGGBB)", location="", code="INVALID_COLOR_FORMAT" )) return issues @dataclass class TableElement: """表格元素""" type: str = 'table' data: list = field(default_factory=list) position: list = field(default_factory=lambda: [1, 1]) col_widths: list = field(default_factory=list) style: dict = field(default_factory=dict) font: Union[FontConfig, str, None] = None header_font: Union[FontConfig, str, None] = None def __post_init__(self): """创建时验证""" if not self.data: raise ValueError("表格数据不能为空") if not isinstance(self.position, list) or len(self.position) != 2: raise ValueError("position 必须是包含 2 个数字的列表") def validate(self) -> List: """验证元素自身的完整性""" from validators.result import ValidationIssue issues = [] # 检查表格数据行列数一致性 if self.data: first_row_cols = len(self.data[0]) if isinstance(self.data[0], list) else 0 for i, row in enumerate(self.data[1:], start=1): if isinstance(row, list): if len(row) != first_row_cols: issues.append(ValidationIssue( level="ERROR", message=f"表格数据行列数不一致: 第 {i+1} 行有 {len(row)} 列,期望 {first_row_cols} 列", location="", code="TABLE_INCONSISTENT_COLUMNS" )) # 检查 col_widths 与列数是否匹配 if self.col_widths and self.data: first_row_cols = len(self.data[0]) if isinstance(self.data[0], list) else 0 if len(self.col_widths) != first_row_cols: issues.append(ValidationIssue( level="WARNING", message=f"col_widths 数量 ({len(self.col_widths)}) 与表格列数 ({first_row_cols}) 不匹配", location="", code="TABLE_COL_WIDTHS_MISMATCH" )) return issues def create_element(elem_dict: dict): """ 元素工厂函数,从字典创建对应类型的元素对象 Args: elem_dict: 元素字典,必须包含 type 字段 Returns: 对应类型的元素对象 Raises: ValueError: 不支持的元素类型 """ elem_type = elem_dict.get('type') # 转换 font 字段为 FontConfig 对象(如果是字典) if 'font' in elem_dict and isinstance(elem_dict['font'], dict): elem_dict = elem_dict.copy() # 避免修改原字典 elem_dict['font'] = FontConfig(**elem_dict['font']) # 转换表格的 font 和 header_font 字段 if elem_type == 'table': elem_dict = elem_dict.copy() if 'font' in elem_dict and isinstance(elem_dict['font'], dict): elem_dict['font'] = FontConfig(**elem_dict['font']) if 'header_font' in elem_dict and isinstance(elem_dict['header_font'], dict): elem_dict['header_font'] = FontConfig(**elem_dict['header_font']) if elem_type == 'text': return TextElement(**elem_dict) elif elem_type == 'image': return ImageElement(**elem_dict) elif elem_type == 'shape': return ShapeElement(**elem_dict) elif elem_type == 'table': return TableElement(**elem_dict) else: raise ValueError(f"不支持的元素类型: {elem_type}")