1
0
Files
PPTX/core/elements.py
lanyuanxiaoyao 19d6661381 feat: 添加图片适配模式支持
- 支持四种图片适配模式: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 模式
2026-03-04 10:29:21 +08:00

246 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
元素抽象层模块
定义统一的元素数据类,支持元素验证和未来扩展。
"""
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}")