1
0
Files
PPTX/yaml2pptx.py
lanyuanxiaoyao bd12fce14b feat: 实现字体主题系统和东亚字体支持
实现完整的字体主题系统,支持可复用字体配置、预设类别和扩展属性。
同时修复中文字体渲染问题,确保 Source Han Sans 等东亚字体正确显示。

核心功能:
- 字体主题配置:metadata.fonts 和 fonts_default
- 三种引用方式:整体引用、继承覆盖、独立定义
- 预设字体类别:sans、serif、mono、cjk-sans、cjk-serif
- 扩展字体属性:family、underline、strikethrough、line_spacing、
  space_before、space_after、baseline、caps
- 表格字体字段:font 和 header_font 替代旧的 style.font_size
- 引用循环检测和属性继承链
- 模板字体继承支持

东亚字体修复:
- 添加 _set_font_with_eastasian() 方法
- 同时设置拉丁字体、东亚字体和复杂脚本字体
- 修复中文字符使用默认字体的问题

测试:
- 58 个单元测试覆盖所有字体系统功能
- 3 个集成测试验证端到端场景
- 移除旧语法相关测试

文档:
- 更新 README.md 添加字体主题系统使用说明
- 更新 README_DEV.md 添加技术文档
- 创建 4 个示例 YAML 文件
- 同步 delta specs 到主 specs

归档:
- 归档 font-theme-system 变更到 openspec/changes/archive/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 10:38:59 +08:00

227 lines
7.3 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.
#!/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, fonts=pres.fonts, fonts_default=pres.fonts_default)
# 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()