- 支持四种图片适配模式: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 模式
227 lines
7.3 KiB
Python
227 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
YAML to PPTX Converter
|
||
将 YAML 格式的演示文稿源文件转换为 PPTX 文件
|
||
|
||
使用方法:
|
||
# 验证 YAML 文件
|
||
uv run yaml2pptx.py check input.yaml
|
||
|
||
# 转换为 PPTX
|
||
uv run yaml2pptx.py convert input.yaml output.pptx
|
||
uv run yaml2pptx.py convert input.yaml # 自动生成 input.pptx
|
||
|
||
# 启动预览服务器
|
||
uv run yaml2pptx.py preview input.yaml
|
||
"""
|
||
|
||
import sys
|
||
import argparse
|
||
import random
|
||
from pathlib import Path
|
||
|
||
# 导入模块
|
||
from utils import log_info, log_success, log_error, log_progress
|
||
from loaders.yaml_loader import YAMLError
|
||
from core.presentation import Presentation
|
||
from renderers.pptx_renderer import PptxGenerator
|
||
from preview.server import start_preview_server
|
||
from validators import Validator
|
||
|
||
|
||
def parse_args():
|
||
"""解析命令行参数"""
|
||
parser = argparse.ArgumentParser(
|
||
description="将 YAML 格式的演示文稿源文件转换为 PPTX 文件"
|
||
)
|
||
subparsers = parser.add_subparsers(dest='command', required=True)
|
||
|
||
# check 子命令
|
||
check_parser = subparsers.add_parser('check', help='验证 YAML 文件')
|
||
check_parser.add_argument("input", type=str, help="输入的 YAML 文件路径")
|
||
check_parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
|
||
|
||
# convert 子命令
|
||
convert_parser = subparsers.add_parser('convert', help='转换 YAML 为 PPTX')
|
||
convert_parser.add_argument("input", type=str, help="输入的 YAML 文件路径")
|
||
convert_parser.add_argument("output", type=str, nargs="?", help="输出的 PPTX 文件路径(可选,默认为输入文件名.pptx)")
|
||
convert_parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
|
||
convert_parser.add_argument("--skip-validation", action="store_true", help="跳过自动验证")
|
||
convert_parser.add_argument("-f", "--force", action="store_true", help="强制覆盖已存在文件")
|
||
|
||
# preview 子命令
|
||
preview_parser = subparsers.add_parser('preview', help='启动预览服务器')
|
||
preview_parser.add_argument("input", type=str, help="输入的 YAML 文件路径")
|
||
preview_parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
|
||
preview_parser.add_argument("--port", type=int, default=None, help="服务器端口(默认:随机端口 30000-40000)")
|
||
preview_parser.add_argument("--host", type=str, default="127.0.0.1", help="主机地址(默认:127.0.0.1)")
|
||
preview_parser.add_argument("--no-browser", action="store_true", help="不自动打开浏览器")
|
||
|
||
return parser.parse_args()
|
||
|
||
|
||
def handle_check(args):
|
||
"""处理 check 子命令"""
|
||
try:
|
||
input_path = Path(args.input)
|
||
|
||
# 处理模板目录
|
||
template_dir = None
|
||
if args.template_dir:
|
||
template_dir = Path(args.template_dir)
|
||
|
||
# 执行验证
|
||
validator = Validator()
|
||
result = validator.validate(input_path, template_dir)
|
||
|
||
# 输出验证结果
|
||
print(result.format_output())
|
||
|
||
# 返回退出码
|
||
if result.has_errors():
|
||
sys.exit(1)
|
||
else:
|
||
sys.exit(0)
|
||
|
||
except Exception as e:
|
||
log_error(f"验证失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
def handle_preview(args):
|
||
"""处理 preview 子命令"""
|
||
# 检查输入文件是否存在
|
||
input_path = Path(args.input)
|
||
if not input_path.exists():
|
||
log_error(f"输入文件不存在: {input_path}")
|
||
sys.exit(1)
|
||
|
||
# 处理端口:如果未指定,随机选择 30000-40000 范围内的端口
|
||
port = args.port
|
||
if port is None:
|
||
port = random.randint(30000, 40000)
|
||
|
||
# 处理模板目录
|
||
template_dir = Path(args.template_dir) if args.template_dir else None
|
||
|
||
# 启动预览服务器
|
||
start_preview_server(
|
||
yaml_file=input_path,
|
||
template_dir=template_dir,
|
||
port=port,
|
||
host=args.host,
|
||
open_browser=not args.no_browser
|
||
)
|
||
|
||
|
||
def main():
|
||
"""主函数:加载 YAML → 渲染幻灯片 → 生成 PPTX"""
|
||
try:
|
||
args = parse_args()
|
||
|
||
# 路由到对应的处理函数
|
||
if args.command == 'check':
|
||
handle_check(args)
|
||
elif args.command == 'convert':
|
||
handle_convert(args)
|
||
elif args.command == 'preview':
|
||
handle_preview(args)
|
||
|
||
except YAMLError as e:
|
||
log_error(str(e))
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
log_error(f"未知错误: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
def handle_convert(args):
|
||
"""处理 convert 子命令"""
|
||
# 检查是否提供了输入文件
|
||
if not args.input:
|
||
log_error("错误: 缺少输入文件参数")
|
||
sys.exit(1)
|
||
|
||
# 处理输入文件路径
|
||
input_path = Path(args.input)
|
||
|
||
# 检查输入文件是否存在
|
||
if not input_path.exists():
|
||
log_error(f"输入文件不存在: {input_path}")
|
||
sys.exit(1)
|
||
|
||
# 处理输出文件路径
|
||
if args.output:
|
||
output_path = Path(args.output)
|
||
else:
|
||
# 自动生成输出文件名
|
||
output_path = input_path.with_suffix(".pptx")
|
||
|
||
# 检查输出文件是否已存在(除非使用 --force)
|
||
if output_path.exists() and not args.force:
|
||
log_error(f"输出文件已存在: {output_path}")
|
||
log_error("使用 --force 或 -f 强制覆盖")
|
||
sys.exit(1)
|
||
|
||
log_info(f"开始转换: {input_path}")
|
||
log_info(f"输出文件: {output_path}")
|
||
|
||
# 自动验证(除非使用 --skip-validation)
|
||
if not args.skip_validation:
|
||
log_info("验证 YAML 文件...")
|
||
template_dir = Path(args.template_dir) if args.template_dir else None
|
||
validator = Validator()
|
||
result = validator.validate(input_path, template_dir)
|
||
|
||
# 如果有错误,输出并终止
|
||
if result.has_errors():
|
||
print("\n" + result.format_output())
|
||
log_error("验证失败,转换已终止")
|
||
sys.exit(1)
|
||
|
||
# 如果有警告,输出但继续
|
||
if result.warnings:
|
||
print("\n" + result.format_output())
|
||
print() # 空行
|
||
|
||
# 1. 加载演示文稿
|
||
log_info("加载演示文稿...")
|
||
pres = Presentation(input_path, templates_dir=args.template_dir)
|
||
|
||
# 2. 创建 PPTX 生成器
|
||
log_info(f"创建演示文稿 ({pres.size})...")
|
||
generator = PptxGenerator(pres.size, pres.dpi)
|
||
|
||
# 3. 渲染所有幻灯片
|
||
slides_data = pres.data.get('slides', [])
|
||
total_slides = len(slides_data)
|
||
|
||
# 统计实际渲染的幻灯片数量
|
||
enabled_slides = [s for s in slides_data if s.get('enabled', True)]
|
||
enabled_count = len(enabled_slides)
|
||
|
||
slide_index = 0
|
||
for i, slide_data in enumerate(slides_data, 1):
|
||
# 检查页面级 enabled
|
||
if not slide_data.get('enabled', True):
|
||
continue # 跳过禁用的页面
|
||
|
||
slide_index += 1
|
||
log_progress(slide_index, enabled_count, f"处理幻灯片")
|
||
rendered_slide = pres.render_slide(slide_data)
|
||
generator.add_slide(rendered_slide, input_path.parent)
|
||
|
||
# 4. 保存 PPTX 文件
|
||
log_info("保存 PPTX 文件...")
|
||
generator.save(output_path)
|
||
|
||
log_success(f"转换完成: {output_path}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|