- 支持四种图片适配模式:stretch、contain、cover、center - 支持背景色填充功能(contain 和 center 模式) - 支持文档级 DPI 配置(metadata.dpi) - PPTX 渲染器集成 Pillow 实现高质量图片处理 - HTML 渲染器使用 CSS object-fit 实现相同效果 - 添加完整的单元测试、集成测试和端到端测试 - 更新 README 文档和架构文档 - 模块化设计:utils/image_utils.py 图片处理工具模块 - 添加图片配置验证器:validators/image_config.py - 向后兼容:未指定 fit 时默认使用 stretch 模式
246 lines
8.0 KiB
Python
246 lines
8.0 KiB
Python
"""
|
||
元素抽象层模块
|
||
|
||
定义统一的元素数据类,支持元素验证和未来扩展。
|
||
"""
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional, List
|
||
import re
|
||
|
||
|
||
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: 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 = []
|
||
|
||
# 检查颜色格式
|
||
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])
|
||
fit: Optional[str] = None
|
||
background: Optional[str] = None
|
||
|
||
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:
|
||
"""验证元素自身的完整性"""
|
||
from validators.result import ValidationIssue
|
||
issues = []
|
||
|
||
# 验证 fit 参数
|
||
if self.fit is not None:
|
||
valid_fits = ['stretch', 'contain', 'cover', 'center']
|
||
if self.fit not in valid_fits:
|
||
issues.append(ValidationIssue(
|
||
level="ERROR",
|
||
message=f"无效的 fit 值: {self.fit} (支持: {', '.join(valid_fits)})",
|
||
location="",
|
||
code="INVALID_FIT_VALUE"
|
||
))
|
||
|
||
# 验证 background 参数
|
||
if self.background is not None:
|
||
if not _is_valid_color(self.background):
|
||
issues.append(ValidationIssue(
|
||
level="ERROR",
|
||
message=f"无效的背景颜色格式: {self.background} (应为 #RRGGBB 或 #RGB)",
|
||
location="",
|
||
code="INVALID_COLOR_FORMAT"
|
||
))
|
||
|
||
return issues
|
||
|
||
|
||
@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)
|
||
|
||
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')
|
||
|
||
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}")
|