1
0

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 模式
This commit is contained in:
2026-03-04 10:29:21 +08:00
parent 16ca9d77cd
commit 19d6661381
32 changed files with 2310 additions and 57 deletions

View File

@@ -8,13 +8,18 @@ from pathlib import Path
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
# 固定 DPI 用于单位转换
DPI = 96
class HtmlRenderer:
"""HTML 渲染器,将元素渲染为 HTML"""
def __init__(self, dpi=96):
"""
初始化 HTML 渲染器
Args:
dpi: DPI 配置,默认 96
"""
self.dpi = dpi
def render_slide(self, slide_data, index, base_path):
"""
渲染单个幻灯片为 HTML
@@ -68,10 +73,10 @@ class HtmlRenderer:
str: HTML 代码
"""
style = f"""
left: {elem.box[0] * DPI}px;
top: {elem.box[1] * DPI}px;
width: {elem.box[2] * DPI}px;
height: {elem.box[3] * DPI}px;
left: {elem.box[0] * self.dpi}px;
top: {elem.box[1] * self.dpi}px;
width: {elem.box[2] * self.dpi}px;
height: {elem.box[3] * self.dpi}px;
font-size: {elem.font.get("size", 16)}pt;
color: {elem.font.get("color", "#000000")};
text-align: {elem.font.get("align", "left")};
@@ -105,10 +110,10 @@ class HtmlRenderer:
}.get(elem.shape, "0")
style = f"""
left: {elem.box[0] * DPI}px;
top: {elem.box[1] * DPI}px;
width: {elem.box[2] * DPI}px;
height: {elem.box[3] * DPI}px;
left: {elem.box[0] * self.dpi}px;
top: {elem.box[1] * self.dpi}px;
width: {elem.box[2] * self.dpi}px;
height: {elem.box[3] * self.dpi}px;
background: {elem.fill if elem.fill else "transparent"};
border-radius: {border_radius};
"""
@@ -131,8 +136,8 @@ class HtmlRenderer:
str: HTML 代码
"""
table_style = f"""
left: {elem.position[0] * DPI}px;
top: {elem.position[1] * DPI}px;
left: {elem.position[0] * self.dpi}px;
top: {elem.position[1] * self.dpi}px;
"""
rows_html = ""
@@ -166,11 +171,46 @@ class HtmlRenderer:
"""
img_path = Path(base_path) / elem.src if base_path else Path(elem.src)
# 获取 fit 模式,默认为 stretch
fit = elem.fit if elem.fit else 'stretch'
# fit 模式到 CSS object-fit 的映射
object_fit_map = {
'stretch': 'fill',
'contain': 'contain',
'cover': 'cover',
'center': 'none'
}
object_fit = object_fit_map.get(fit, 'fill')
# 基础样式
style = f"""
left: {elem.box[0] * DPI}px;
top: {elem.box[1] * DPI}px;
width: {elem.box[2] * DPI}px;
height: {elem.box[3] * DPI}px;
left: {elem.box[0] * self.dpi}px;
top: {elem.box[1] * self.dpi}px;
width: {elem.box[2] * self.dpi}px;
height: {elem.box[3] * self.dpi}px;
object-fit: {object_fit};
object-position: center;
"""
return f'<img class="element image-element" src="file://{img_path.absolute()}" style="{style}">'
# 如果有背景色,需要创建包装容器
if elem.background:
container_style = f"""
position: absolute;
left: {elem.box[0] * self.dpi}px;
top: {elem.box[1] * self.dpi}px;
width: {elem.box[2] * self.dpi}px;
height: {elem.box[3] * self.dpi}px;
background-color: {elem.background};
"""
img_style = f"""
width: 100%;
height: 100%;
object-fit: {object_fit};
object-position: center;
"""
return f'''<div class="element image-container" style="{container_style}">
<img class="image-element" src="file://{img_path.absolute()}" style="{img_style}">
</div>'''
else:
return f'<img class="element image-element" src="file://{img_path.absolute()}" style="{style}">'