- 支持四种图片适配模式: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 模式
217 lines
6.9 KiB
Python
217 lines
6.9 KiB
Python
"""
|
|
HTML 渲染器模块
|
|
|
|
将元素对象渲染为 HTML 代码,用于浏览器预览。
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
|
|
|
|
|
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
|
|
|
|
Args:
|
|
slide_data: 包含 background 和 elements 的字典
|
|
index: 幻灯片索引
|
|
base_path: 基础路径
|
|
|
|
Returns:
|
|
str: 幻灯片的 HTML 代码
|
|
"""
|
|
elements_html = ""
|
|
|
|
bg_style = ""
|
|
if slide_data.get("background"):
|
|
bg = slide_data["background"]
|
|
if "color" in bg:
|
|
bg_style = f"background: {bg['color']};"
|
|
|
|
for elem in slide_data.get("elements", []):
|
|
try:
|
|
if isinstance(elem, TextElement):
|
|
elements_html += self.render_text(elem)
|
|
elif isinstance(elem, ShapeElement):
|
|
elements_html += self.render_shape(elem)
|
|
elif isinstance(elem, TableElement):
|
|
elements_html += self.render_table(elem)
|
|
elif isinstance(elem, ImageElement):
|
|
elements_html += self.render_image(elem, base_path)
|
|
except Exception as e:
|
|
elements_html += (
|
|
f'<div class="element" style="color: red;">渲染错误: {str(e)}</div>'
|
|
)
|
|
|
|
return f'''
|
|
<div class="slide" style="{bg_style}">
|
|
<div class="slide-number">幻灯片 {index + 1}</div>
|
|
{elements_html}
|
|
</div>
|
|
'''
|
|
|
|
def render_text(self, elem: TextElement):
|
|
"""
|
|
将文本元素转换为 HTML
|
|
|
|
Args:
|
|
elem: TextElement 对象
|
|
|
|
Returns:
|
|
str: HTML 代码
|
|
"""
|
|
style = f"""
|
|
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")};
|
|
{"font-weight: bold;" if elem.font.get("bold") else ""}
|
|
{"font-style: italic;" if elem.font.get("italic") else ""}
|
|
display: flex;
|
|
align-items: center;
|
|
white-space: normal;
|
|
overflow-wrap: break-word;
|
|
"""
|
|
|
|
content = (
|
|
elem.content.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
)
|
|
return f'<div class="element text-element" style="{style}">{content}</div>'
|
|
|
|
def render_shape(self, elem: ShapeElement):
|
|
"""
|
|
将形状元素转换为 HTML
|
|
|
|
Args:
|
|
elem: ShapeElement 对象
|
|
|
|
Returns:
|
|
str: HTML 代码
|
|
"""
|
|
border_radius = {
|
|
"rectangle": "0",
|
|
"ellipse": "50%",
|
|
"rounded_rectangle": "8px",
|
|
}.get(elem.shape, "0")
|
|
|
|
style = f"""
|
|
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};
|
|
"""
|
|
|
|
if elem.line:
|
|
style += f"""
|
|
border: {elem.line.get("width", 1)}pt solid {elem.line.get("color", "#000000")};
|
|
"""
|
|
|
|
return f'<div class="element shape-element" style="{style}"></div>'
|
|
|
|
def render_table(self, elem: TableElement):
|
|
"""
|
|
将表格元素转换为 HTML
|
|
|
|
Args:
|
|
elem: TableElement 对象
|
|
|
|
Returns:
|
|
str: HTML 代码
|
|
"""
|
|
table_style = f"""
|
|
left: {elem.position[0] * self.dpi}px;
|
|
top: {elem.position[1] * self.dpi}px;
|
|
"""
|
|
|
|
rows_html = ""
|
|
for i, row in enumerate(elem.data):
|
|
cells_html = ""
|
|
for cell in row:
|
|
cell_style = f"font-size: {elem.style.get('font_size', 14)}pt;"
|
|
|
|
if i == 0:
|
|
if "header_bg" in elem.style:
|
|
cell_style += f"background: {elem.style['header_bg']};"
|
|
if "header_color" in elem.style:
|
|
cell_style += f"color: {elem.style['header_color']};"
|
|
|
|
cell_content = str(cell).replace("<", "<").replace(">", ">")
|
|
cells_html += f'<td style="{cell_style}">{cell_content}</td>'
|
|
rows_html += f"<tr>{cells_html}</tr>"
|
|
|
|
return f'<table class="element table-element" style="{table_style}">{rows_html}</table>'
|
|
|
|
def render_image(self, elem: ImageElement, base_path):
|
|
"""
|
|
将图片元素转换为 HTML
|
|
|
|
Args:
|
|
elem: ImageElement 对象
|
|
base_path: 基础路径
|
|
|
|
Returns:
|
|
str: HTML 代码
|
|
"""
|
|
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] * 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;
|
|
"""
|
|
|
|
# 如果有背景色,需要创建包装容器
|
|
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}">'
|