1
0
Files
PPTX/preview/server.py
lanyuanxiaoyao f34405be36 feat: 移除图片适配模式功能
移除图片 fit 和 background 参数支持,简化图片渲染逻辑。系统恢复到直接使用 python-pptx 原生图片添加功能,图片将被拉伸到指定尺寸。

变更内容:
- 移除 ImageElement 的 fit 和 background 字段
- 移除 metadata.dpi 配置
- 删除 utils/image_utils.py 图片处理工具模块
- 删除 validators/image_config.py 验证器
- 简化 PPTX 和 HTML 渲染器的图片处理逻辑
- HTML 渲染器使用硬编码 DPI=96(Web 标准)
- 删除相关测试文件(单元测试、集成测试、e2e 测试)
- 更新规格文档和用户文档
- 保留 Pillow 依赖用于未来可能的图片处理需求

影响:
- 删除 11 个文件
- 修改 10 个文件
- 净减少 1558 行代码
- 所有 402 个测试通过

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-04 14:23:12 +08:00

250 lines
6.6 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.
"""
预览服务器模块
提供浏览器预览功能,支持文件监听和热重载。
"""
import sys
import queue
import webbrowser
import random
from pathlib import Path
from threading import Thread
try:
from flask import Flask, Response
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
except ImportError:
Flask = None
Observer = None
FileSystemEventHandler = None
from core.presentation import Presentation
from renderers.html_renderer import HtmlRenderer
from loaders.yaml_loader import YAMLError
from utils import log_info, log_error
# HTML 模板
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>YAML Preview</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 20px;
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.slide {
width: 960px;
height: 540px;
position: relative;
background: white;
margin: 20px auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.slide-number {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 12px;
color: #999;
z-index: 1000;
}
.element {
position: absolute;
}
table.element {
border-collapse: collapse;
}
table.element td {
padding: 8px;
border: 1px solid #ddd;
}
</style>
</head>
<body>
{{ slides_html }}
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = (e) => {
if (e.data === 'reload') {
console.log('[Preview] 重新加载...');
location.reload();
}
};
eventSource.onerror = () => {
console.error('[Preview] 连接断开');
};
</script>
</body>
</html>
"""
ERROR_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>预览错误</title>
<style>
body {
margin: 0;
padding: 40px;
background: #f5f5f5;
font-family: monospace;
}
.error {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 4px;
color: #856404;
}
h1 { margin-top: 0; }
</style>
</head>
<body>
<div class="error">
<h1>⚠️ YAML 解析错误</h1>
<pre>{{ error }}</pre>
</div>
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = () => location.reload();
</script>
</body>
</html>
"""
# 全局变量
app = None
change_queue = None
current_yaml_file = None
current_template_dir = None
class YAMLChangeHandler:
"""文件变化处理器"""
def on_modified(self, event):
if event.src_path.endswith('.yaml'):
log_info(f"检测到文件变化: {event.src_path}")
if change_queue:
change_queue.put('reload')
def generate_preview_html(yaml_file, template_dir):
"""生成完整的预览 HTML 页面"""
try:
pres = Presentation(yaml_file, template_dir)
renderer = HtmlRenderer()
slides_html = ""
for i, slide_data in enumerate(pres.data.get('slides', [])):
rendered = pres.render_slide(slide_data)
slides_html += renderer.render_slide(rendered, i, Path(yaml_file).parent)
return HTML_TEMPLATE.replace('{{ slides_html }}', slides_html)
except YAMLError as e:
return ERROR_TEMPLATE.replace('{{ error }}', str(e))
def create_flask_app():
"""创建 Flask 应用"""
flask_app = Flask(__name__)
@flask_app.route('/')
def index():
"""主页面"""
try:
return generate_preview_html(current_yaml_file, current_template_dir)
except Exception as e:
return ERROR_TEMPLATE.replace('{{ error }}', f"生成预览失败: {str(e)}")
@flask_app.route('/events')
def events():
"""SSE 事件流"""
def event_stream():
while True:
change_queue.get()
yield 'data: reload\\n\\n'
return Response(event_stream(), mimetype='text/event-stream')
return flask_app
def start_preview_server(yaml_file, template_dir, port, host='127.0.0.1', open_browser=True):
"""启动预览服务器
Args:
yaml_file: YAML 文件路径
template_dir: 模板目录路径
port: 服务器端口
host: 主机地址默认127.0.0.1
open_browser: 是否自动打开浏览器默认True
"""
global app, change_queue, current_yaml_file, current_template_dir
if Flask is None:
log_error("预览功能需要 flask 和 watchdog 依赖")
log_error("请确保使用 uv 运行脚本,依赖会自动安装")
sys.exit(1)
current_yaml_file = yaml_file
current_template_dir = template_dir
change_queue = queue.Queue()
# 创建 Flask 应用
app = create_flask_app()
# 启动文件监听
if FileSystemEventHandler:
handler = YAMLChangeHandler()
if hasattr(handler, 'on_modified'):
# 创建一个简单的事件处理器
class SimpleHandler(FileSystemEventHandler):
def on_modified(self, event):
handler.on_modified(event)
observer = Observer()
observer.schedule(SimpleHandler(), str(Path(yaml_file).parent), recursive=False)
observer.start()
# 输出日志
log_info(f"正在监听: {yaml_file}")
log_info(f"预览地址: http://{host}:{port}")
log_info("按 Ctrl+C 停止")
# 自动打开浏览器(如果启用)
if open_browser:
Thread(target=lambda: webbrowser.open(f'http://localhost:{port}')).start()
# 启动 Flask
try:
app.run(host=host, port=port, debug=False, threaded=True)
except OSError as e:
if 'Address already in use' in str(e):
log_error(f"端口 {port} 已被占用")
log_error(f"请使用 --port 参数指定其他端口,例如: --port {port + 1}")
else:
log_error(f"启动服务器失败: {str(e)}")
sys.exit(1)
except KeyboardInterrupt:
if 'observer' in locals():
observer.stop()
observer.join()
log_info("已停止")