# Universal PPTX Generator

> 通用 PPT 生成技能。能够根据用户指定的任意 PPT 模板，结合提供的图文素材（文档、图片等），自动生成一份风格统一的演示文稿。支持自动分析模板结构、提取背景图/背景色、解析配色方案和字体规范，然后根据素材内容生成完整 PPT。支持多种图表类型（柱状图、折线图、饼图、雷达图等）的数据可视化展示。关键词：PPT生成、模板分析、演示文稿、幻灯片、图文排版、自动化、图表、数据可视化。

- Skill: `ajaxhe/universal-pptx-generator` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add ajaxhe/universal-pptx-generator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ajaxhe/universal-pptx-generator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: ajaxhe (https://skillmd.com/u/ajaxhe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ajaxhe/universal-pptx-generator

---


# 通用 PPT 生成技能

## 概述

此技能可以根据**任意用户指定的 PPT 模板**，结合提供的**图文素材**（文档、图片等），自动生成一份风格统一的演示文稿。

**⭐⭐⭐ 核心理念：每个模板都是独特的，必须针对性分析！**

不同 PPT 模板使用的字体、对齐方式、字号、颜色、位置、**背景样式**都完全不同。**绝不能**将一个模板的配置直接应用到另一个模板！每次使用新模板时，都必须重新分析 XML 提取精确参数。

**核心能力:**
1. **模板分析** - 自动解析 PPTX 模板结构、配色、字体、**背景图/背景色**、对齐方式
2. **⭐⭐⭐ 分页面类型分析** - 针对封面、目录、章节、内容、结束页分别提取背景和样式
3. **素材处理** - 从 DOCX/PDF/图片等素材中提取内容
4. **智能排版** - 根据模板风格自动排版生成内容
5. **批量生成** - 支持生成多页完整演示文稿
6. **⭐ 图表展示** - 支持柱状图、折线图、饼图、雷达图等多种数据可视化图表

**关键词**: PPT生成、模板分析、演示文稿、幻灯片、图文排版、自动化、pptxgenjs、图表、数据可视化

---

## ⭐⭐⭐ 页面类型与背景处理 (关键！)

### 五种核心页面类型

不同类型的页面可能使用不同的背景处理方式：

| 页面类型 | 典型特征 | 背景处理方式 |
|---------|---------|-------------|
| **封面页 (Cover)** | 主标题 + 副标题 + Logo | 背景图/渐变/斜切形状 |
| **目录页 (TOC)** | 目录列表 + 装饰元素 | 纯色背景 + 装饰形状 |
| **章节页 (Chapter)** | 大号章节编号 + 章节标题 | 纯色背景 + 装饰形状 |
| **内容页 (Content)** | 标题 + 正文/图片/图表 | 纯色背景/背景图 |
| **结束页 (Thanks)** | 感谢语 + 联系方式 | 纯色背景 + 装饰形状 |

### ⭐⭐⭐ 背景类型分析

PPT 背景有三种主要类型：

#### 1. 纯色背景 (SolidFill)

```xml
<!-- XML 特征 -->
<p:bg>
  <p:bgPr>
    <a:solidFill>
      <a:schemeClr val="tx2"/>  <!-- 使用主题色 -->
    </a:solidFill>
  </p:bgPr>
</p:bg>

<!-- 或直接指定颜色 -->
<a:solidFill>
  <a:srgbClr val="0D1E43"/>  <!-- 直接 RGB 值 -->
</a:solidFill>
```

**pptxgenjs 对应代码：**
```javascript
slide.background = { color: '0D1E43' };  // 深蓝色
```

#### 2. 背景图片 (BlipFill)

```xml
<!-- XML 特征 -->
<p:bg>
  <p:bgPr>
    <a:blipFill>
      <a:blip r:embed="rId2"/>  <!-- 引用图片资源 -->
    </a:blipFill>
  </p:bgPr>
</p:bg>

<!-- 或在形状中作为图片填充 -->
<p:pic>
  <p:blipFill>
    <a:blip r:embed="rId2"/>
  </p:blipFill>
</p:pic>
```

**pptxgenjs 对应代码：**
```javascript
slide.background = { path: 'workspace/backgrounds/cover-bg.png' };
```

#### 3. 渐变背景 (GradFill)

```xml
<!-- XML 特征 -->
<a:gradFill>
  <a:gsLst>
    <a:gs pos="0">
      <a:srgbClr val="0052D9"><a:alpha val="50000"/></a:srgbClr>
    </a:gs>
    <a:gs pos="100000">
      <a:srgbClr val="0D1E43"/>
    </a:gs>
  </a:gsLst>
  <a:lin ang="5400000"/>  <!-- 角度：5400000/60000 = 90° -->
</a:gradFill>
```

**pptxgenjs 对应代码：**
```javascript
slide.background = {
  color: '0D1E43',  // 基础色
  // 注：pptxgenjs 对渐变背景支持有限，通常用形状模拟
};

// 用形状模拟渐变
slide.addShape('rect', {
  x: 0, y: 0, w: '100%', h: '100%',
  fill: {
    type: 'gradient',
    gradientType: 'linear',
    degrees: 90,
    stops: [
      { position: 0, color: '0052D9', alpha: 50 },
      { position: 100, color: '0D1E43' }
    ]
  }
});
```

### 主题色映射

很多模板使用主题色（schemeClr）而非直接颜色值：

| schemeClr 值 | 含义 | 典型颜色 |
|-------------|------|---------|
| `dk1` | 深色1 (主要文字) | 000000 |
| `lt1` / `bg1` | 浅色1 (背景) | FFFFFF |
| `dk2` / `tx2` | 深色2 (次要背景) | 0060FF / 0D1E43 |
| `lt2` | 浅色2 | E7E6E6 |
| `accent1` | 强调色1 | 0060F0 |
| `accent2` | 强调色2 | A736FF |

**从 theme1.xml 提取主题色映射：**
```bash
cat workspace/template-analysis/ppt/theme/theme1.xml | grep -E "dk1|lt1|dk2|lt2|accent" | head -20
```

### ⭐⭐⭐ 分页面类型完整分析流程

```python
# 完整的页面背景分析脚本
import re
import os

def analyze_slide_background(slide_path, rels_path):
    """分析单页幻灯片的背景类型"""
    
    with open(slide_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    result = {
        'background_type': None,
        'background_color': None,
        'background_image': None,
        'decorative_shapes': [],
        'scheme_color': None
    }
    
    # 1. 检查是否有 <p:bg> 背景定义
    bg_match = re.search(r'<p:bg>(.*?)</p:bg>', content, re.DOTALL)
    if bg_match:
        bg_content = bg_match.group(1)
        
        # 纯色背景
        if '<a:solidFill>' in bg_content:
            result['background_type'] = 'solid'
            # 直接颜色
            color = re.search(r'srgbClr val="([^"]+)"', bg_content)
            if color:
                result['background_color'] = color.group(1)
            # 主题色
            scheme = re.search(r'schemeClr val="([^"]+)"', bg_content)
            if scheme:
                result['scheme_color'] = scheme.group(1)
        
        # 渐变背景
        elif '<a:gradFill>' in bg_content:
            result['background_type'] = 'gradient'
        
        # 图片背景
        elif '<a:blipFill>' in bg_content:
            result['background_type'] = 'image'
            embed = re.search(r'r:embed="([^"]+)"', bg_content)
            if embed:
                result['background_image'] = embed.group(1)
    
    # 2. 检查是否有全屏图片作为背景（无 <p:bg> 时）
    if not result['background_type']:
        pics = re.findall(r'<p:pic>(.*?)</p:pic>', content, re.DOTALL)
        for pic in pics:
            # 检查是否是大尺寸图片（可能是背景图）
            ext_match = re.search(r'<a:ext cx="(\d+)" cy="(\d+)"/>', pic)
            if ext_match:
                cx, cy = int(ext_match.group(1)), int(ext_match.group(2))
                # 如果尺寸接近全屏（>80%），视为背景图
                if cx > 10000000 and cy > 5000000:  # EMU 单位
                    result['background_type'] = 'image_shape'
                    embed = re.search(r'r:embed="([^"]+)"', pic)
                    if embed:
                        result['background_image'] = embed.group(1)
                    break
    
    # 3. 如果还是没有找到背景，检查是否使用装饰形状构成背景
    if not result['background_type']:
        result['background_type'] = 'shapes_composite'
    
    # 4. 分析装饰性形状（半透明方块等）
    shapes = re.findall(r'<p:sp>(.*?)</p:sp>', content, re.DOTALL)
    for shape in shapes:
        # 检查是否有透明度设置
        alpha = re.search(r'<a:alpha val="(\d+)"', shape)
        if alpha:
            transparency = 100 - int(alpha.group(1)) / 1000
            color = re.search(r'srgbClr val="([^"]+)"', shape)
            if color and transparency > 0:
                result['decorative_shapes'].append({
                    'color': color.group(1),
                    'transparency': transparency
                })
    
    # 5. 从 rels 文件获取实际图片路径
    if result['background_image'] and os.path.exists(rels_path):
        with open(rels_path, 'r', encoding='utf-8') as f:
            rels_content = f.read()
        img_id = result['background_image']
        img_path = re.search(rf'{img_id}"[^>]*Target="([^"]+)"', rels_content)
        if img_path:
            result['background_image_path'] = img_path.group(1)
    
    return result

# 分析所有关键页面
def analyze_all_pages(template_dir):
    slides_dir = f'{template_dir}/ppt/slides'
    rels_dir = f'{slides_dir}/_rels'
    
    page_types = {
        1: '封面页',
        2: '目录页',
        3: '章节页',
        4: '内容页'
    }
    
    results = {}
    for slide_num, page_type in page_types.items():
        slide_path = f'{slides_dir}/slide{slide_num}.xml'
        rels_path = f'{rels_dir}/slide{slide_num}.xml.rels'
        
        if os.path.exists(slide_path):
            result = analyze_slide_background(slide_path, rels_path)
            result['page_type'] = page_type
            results[f'slide{slide_num}'] = result
            
            print(f"\n=== {page_type} (slide{slide_num}) ===")
            print(f"背景类型: {result['background_type']}")
            if result['background_color']:
                print(f"背景颜色: #{result['background_color']}")
            if result['scheme_color']:
                print(f"主题色引用: {result['scheme_color']}")
            if result['background_image_path']:
                print(f"背景图片: {result['background_image_path']}")
            if result['decorative_shapes']:
                print(f"装饰形状: {len(result['decorative_shapes'])}个")
                for s in result['decorative_shapes'][:3]:
                    print(f"  - 颜色: #{s['color']}, 透明度: {s['transparency']}%")
    
    return results

# 执行分析
# results = analyze_all_pages('workspace/template-analysis')
```

## 工作流程

```
┌─────────────────────────────────────────────────────────────┐
│                     通用 PPT 生成流程                        │
├─────────────────────────────────────────────────────────────┤
│  1. 模板深度分析阶段 (关键!)                                 │
│     └── 解压 PPTX → 精确分析每页 XML → 提取字号/颜色/位置    │
│                                                              │
│  2. 素材处理阶段                                             │
│     └── 解析 DOCX/PDF → 提取文本/图片 → 结构化内容           │
│                                                              │
│  3. 内容规划阶段                                             │
│     └── 分析内容 → 设计结构 → 分配页面                       │
│     └── ⭐ 识别数据 → 选择图表类型 → 准备图表数据            │
│                                                              │
│  4. PPT 生成阶段                                             │
│     └── 应用精确的模板参数 → 填充内容 → 生成图表 → 输出 PPTX │
│                                                              │
│  5. 清理阶段                                                 │
│     └── 删除临时文件 → 保留最终 PPTX                         │
└─────────────────────────────────────────────────────────────┘
```

---

## 第一阶段：模板深度分析 (最关键!)

### ⚠️ 核心原则

**必须精确提取模板中每一页的实际参数，而非使用推测值！**

分析时需要从 slide XML 中提取：
1. **精确字号** - `sz="8000"` 表示 80pt (sz ÷ 100)
2. **精确位置** - `<a:off x="..." y="..."/>` EMU单位，转换为英寸或百分比
3. **精确颜色** - `srgbClr val="FFFFFF"` 直接使用
4. **字体粗细** - `b="1"` 表示粗体
5. **⭐ 对齐方式** - `algn="ctr"` 居中, `algn="r"` 右对齐, `algn="l"` 或无属性表示左对齐

### 1.1 解压 PPTX 模板

PPTX 文件本质是 ZIP 压缩包，包含 XML 和媒体资源：

```bash
# 创建工作目录
mkdir -p workspace/template-analysis workspace/backgrounds workspace/images

# 解压模板 (使用 Python 处理特殊字符文件名)
python3 -c "
import zipfile
import glob
files = glob.glob('模板*.pptx')
if files:
    with zipfile.ZipFile(files[0], 'r') as zip_ref:
        zip_ref.extractall('workspace/template-analysis')
    print('Done:', files[0])
"
```

### 1.2 PPTX 文件结构详解

```
template-analysis/
├── [Content_Types].xml      # 内容类型定义
├── _rels/
├── docProps/
│   ├── app.xml              # 应用属性
│   └── core.xml             # 核心属性（作者、创建时间等）
└── ppt/
    ├── presentation.xml     # 演示文稿主配置（尺寸、幻灯片列表）
    ├── presProps.xml        # 演示属性
    ├── tableStyles.xml      # 表格样式
    ├── viewProps.xml        # 视图属性
    ├── _rels/
    │   └── presentation.xml.rels  # ⭐ 幻灯片关系映射
    ├── media/               # ⭐ 媒体资源（背景图、图片）
    │   ├── image1.png
    │   ├── image2.png
    │   └── ...
    ├── slideLayouts/        # ⭐ 幻灯片布局定义
    │   ├── slideLayout1.xml   # 封面布局
    │   ├── slideLayout2.xml   # 内容布局
    │   └── ...
    ├── slideMasters/        # ⭐ 母版定义（全局样式）
    │   └── slideMaster1.xml
    ├── slides/              # ⭐⭐⭐ 幻灯片内容 (最重要!)
    │   ├── slide1.xml
    │   ├── slide2.xml
    │   └── ...
    └── theme/               # ⭐ 主题配色/字体
        └── theme1.xml
```

### 1.3 精确分析每页幻灯片 (核心步骤!)

**⚠️ 关键：必须分析每一页的实际 XML，提取精确参数！**

**⚠️⚠️⚠️ 特别重要：必须提取每个元素的字体名称！**

**⚠️⚠️⚠️ 新增：必须分析形状边框和背景渐变！**

使用 Python 脚本精确提取参数（**包含字体、边框、渐变信息**）：

```python
# 分析幻灯片的精确参数（含字体、边框、渐变）
import re
import sys

def analyze_slide(slide_path):
    with open(slide_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 提取文本内容
    texts = re.findall(r'<a:t>([^<]+)</a:t>', content)
    print('=== 文本内容 ===')
    for t in texts:
        if t.strip():
            print(repr(t))
    
    # ⭐⭐⭐ 提取字体名称 - 极其重要！
    latin_fonts = re.findall(r'latin typeface="([^"]+)"', content)
    ea_fonts = re.findall(r'ea typeface="([^"]+)"', content)
    print('\n=== 字体 (Latin/西文) ===')
    for f in set(latin_fonts):
        print(f'  {f}')
    print('\n=== 字体 (EA/东亚) ===')
    for f in set(ea_fonts):
        print(f'  {f}')
    
    # 提取字号 (sz 值 ÷ 100 = pt)
    sizes = re.findall(r'sz="(\d+)"', content)
    print('\n=== 字号 (百分之一点) ===')
    for s in set(sizes):
        print(f'{s} -> {int(s)/100}pt')
    
    # 提取颜色
    colors = re.findall(r'srgbClr val="([^"]+)"', content)
    print('\n=== sRGB颜色 ===')
    for c in set(colors):
        print(f'#{c}')
    
    # 提取位置 (EMU 单位，1英寸=914400 EMU)
    positions = re.findall(r'<a:off x="(\d+)" y="(\d+)"/>', content)
    print('\n=== 位置信息 (转换为20x11.25英寸画布的百分比) ===')
    for i, (x, y) in enumerate(positions):
        x_inch = int(x) / 914400
        y_inch = int(y) / 914400
        x_pct = x_inch / 20 * 100
        y_pct = y_inch / 11.25 * 100
        print(f'{i}: x={x_pct:.1f}%, y={y_pct:.1f}% (x={x_inch:.2f}in, y={y_inch:.2f}in)')
    
    # 提取尺寸
    extents = re.findall(r'<a:ext cx="(\d+)" cy="(\d+)"/>', content)
    print('\n=== 尺寸信息 ===')
    for i, (cx, cy) in enumerate(extents):
        w_inch = int(cx) / 914400
        h_inch = int(cy) / 914400
        w_pct = w_inch / 20 * 100
        h_pct = h_inch / 11.25 * 100
        print(f'{i}: w={w_pct:.1f}%, h={h_pct:.1f}% (w={w_inch:.2f}in, h={h_inch:.2f}in)')
    
    # ⭐⭐⭐ 新增：分析边框设置
    print('\n=== 边框分析 ===')
    lns = re.findall(r'<a:ln[^>]*>(.*?)</a:ln>', content, re.DOTALL)
    for i, ln in enumerate(lns):
        if '<a:noFill/>' in ln:
            print(f'边框{i}: 无边框 (noFill)')
        elif '<a:solidFill>' in ln:
            color = re.search(r'val="([^"]+)"', ln)
            print(f'边框{i}: 有边框，颜色={color.group(1) if color else "未知"}')
        else:
            print(f'边框{i}: ⚠️ 默认黑色边框！需要在生成时设置 line: "none"')
    
    # ⭐⭐⭐ 新增：分析渐变填充
    print('\n=== 渐变分析 ===')
    gradFills = re.findall(r'<a:gradFill[^>]*>(.*?)</a:gradFill>', content, re.DOTALL)
    for i, grad in enumerate(gradFills):
        # 提取角度
        lin = re.search(r'<a:lin ang="(\d+)"', grad)
        if lin:
            angle = int(lin.group(1)) / 60000
            print(f'渐变{i}: 线性渐变，角度={angle}°')
        
        # 提取颜色停止点
        stops = re.findall(r'<a:gs pos="(\d+)">(.*?)</a:gs>', grad, re.DOTALL)
        for pos, gs_content in stops:
            position = int(pos) / 1000
            color = re.search(r'srgbClr val="([^"]+)"', gs_content)
            alpha = re.search(r'<a:alpha val="(\d+)"', gs_content)
            transparency = 100 - int(alpha.group(1))/1000 if alpha else 0
            print(f'  停止点 {position}%: 颜色={color.group(1) if color else "主题色"}, 透明度={transparency}%')
    
    # ⭐⭐⭐ 新增：分析透明度
    print('\n=== 透明度分析 ===')
    alphas = re.findall(r'<a:alpha val="(\d+)"/>', content)
    for a in set(alphas):
        opacity = int(a) / 1000
        transparency = 100 - opacity
        print(f'alpha={a} -> 不透明度={opacity}% -> pptxgenjs transparency={transparency}')
    
    # ⭐⭐⭐ 详细分析每个元素的字体+字号+对齐+文本
    print('\n=== 详细元素分析 (字体+字号+对齐+文本) ===')
    sps = re.findall(r'<p:sp>(.*?)</p:sp>', content, re.DOTALL)
    for i, sp in enumerate(sps):
        texts = re.findall(r'<a:t>([^<]+)</a:t>', sp)
        sizes = re.findall(r'sz="(\d+)"', sp)
        latin = re.findall(r'latin typeface="([^"]+)"', sp)
        ea = re.findall(r'ea typeface="([^"]+)"', sp)
        bold = 'b="1"' in sp
        # ⭐ 提取对齐方式
        algn = re.findall(r'algn="([^"]+)"', sp)
        align_map = {'l': '左对齐', 'ctr': '居中', 'r': '右对齐', 'just': '两端对齐'}
        align_str = align_map.get(algn[0], algn[0]) if algn else '左对齐(默认)'
        # ⭐ 检查是否有边框
        has_border = '<a:ln' in sp and '<a:noFill/>' not in sp
        if texts:
            print(f'元素{i}: 文本="{texts[0][:20]}" 字号={[int(s)/100 for s in sizes[:2]]}pt 字体={latin[:1] or ea[:1]} 粗体={bold} 对齐={align_str} 有边框={has_border}')

# 分析每一页
for i in range(1, 11):
    print(f'\n{"="*60}')
    print(f'SLIDE {i}')
    print("="*60)
    try:
        analyze_slide(f'workspace/template-analysis/ppt/slides/slide{i}.xml')
    except FileNotFoundError:
        print(f'slide{i}.xml not found')
        break
```

### 1.4 分析主题配色

**从 theme1.xml 提取配色方案:**

```bash
cat workspace/template-analysis/ppt/theme/theme1.xml | python3 -c "
import sys
import re
content = sys.stdin.read()

# 提取所有颜色
colors = re.findall(r'srgbClr val=\"([^\"]+)\"', content)
print('主题颜色:')
for c in set(colors):
    print(f'  #{c}')

# 提取字体
fonts = re.findall(r'typeface=\"([^\"]+)\"', content)
print('\n主题字体:')
for f in set(fonts):
    if f:
        print(f'  {f}')
"
```

**关键 XML 结构 - 颜色方案:**
```xml
<a:clrScheme name="主题名称">
  <a:dk1><a:sysClr val="windowText"/></a:dk1>  <!-- 深色1 - 主要文字 -->
  <a:lt1><a:sysClr val="window"/></a:lt1>       <!-- 浅色1 - 背景 -->
  <a:dk2><a:srgbClr val="050E24"/></a:dk2>      <!-- 深色2 -->
  <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>      <!-- 浅色2 -->
  <a:accent1><a:srgbClr val="79E8F5"/></a:accent1>  <!-- 强调色1 - 青色 -->
  <!-- ... -->
</a:clrScheme>
```

### 1.5 识别幻灯片类型与背景图映射

**查看布局关系:**
```bash
for i in 1 2 3 4 5 6 7 8 9 10; do
  echo "=== Slide $i ===" 
  cat workspace/template-analysis/ppt/slides/_rels/slide$i.xml.rels 2>/dev/null | grep -E "(slideLayout|image)"
done
```

**查看布局对应的背景图:**
```bash
for i in 1 2 3 5 6 12; do
  echo "=== slideLayout$i ===" 
  cat workspace/template-analysis/ppt/slideLayouts/_rels/slideLayout$i.xml.rels 2>/dev/null | grep image
done
```

**建立映射表 (示例):**

| 页面类型 | slide | slideLayout | 背景图 |
|----------|-------|-------------|--------|
| 封面页 | slide1 | slideLayout1 | image1.png |
| 目录页 | slide2 | slideLayout5 | image6.png |
| 章节页 | slide3 | slideLayout6 | image7.png |
| 内容页 | slide4 | slideLayout2 | image4.png |
| 感谢页 | slide10 | slideLayout12 | image13.jpeg |

### 1.6 提取背景图

```bash
# 根据分析结果复制背景图
cp workspace/template-analysis/ppt/media/image1.png workspace/backgrounds/cover-bg.png
cp workspace/template-analysis/ppt/media/image6.png workspace/backgrounds/toc-bg.png
cp workspace/template-analysis/ppt/media/image7.png workspace/backgrounds/chapter-bg.png
cp workspace/template-analysis/ppt/media/image4.png workspace/backgrounds/content-bg.png
cp workspace/template-analysis/ppt/media/image13.jpeg workspace/backgrounds/thanks-bg.jpeg
```

---

## 第二阶段：素材处理

### 2.1 处理 DOCX 文档

```bash
# 使用 Python 解压 (处理特殊字符文件名)
python3 -c "
import zipfile
import glob
files = glob.glob('*.docx')
if files:
    with zipfile.ZipFile(files[0], 'r') as zip_ref:
        zip_ref.extractall('workspace/docx-extract')
    print('Done:', files[0])
"

# 提取图片
mkdir -p workspace/images
cp workspace/docx-extract/word/media/*.png workspace/images/ 2>/dev/null
cp workspace/docx-extract/word/media/*.jpeg workspace/images/ 2>/dev/null
```

### 2.2 提取文本内容

```python
import re
with open('workspace/docx-extract/word/document.xml', 'r', encoding='utf-8') as f:
    content = f.read()
texts = re.findall(r'<w:t[^>]*>([^<]+)</w:t>', content)
for t in texts:
    if t.strip():
        print(t.strip())
```

### 2.3 内容结构化

```javascript
const contentStructure = {
  title: "主标题",
  subtitle: "副标题",
  author: "作者",
  chapters: [
    {
      number: "01",
      title: "章节标题",
      subtitle: "章节副标题",
      pages: [
        {
          type: "content",  // content | textOnly | dataCards | chart
          subtitle: "页面小标题",
          title: "页面大标题",
          sections: [
            { title: "要点小标题1", desc: ["描述行1", "描述行2"] },
            { title: "要点小标题2", desc: "单行描述" }
          ],
          image: "images/image1.png"
        },
        // ⭐ 图表页示例
        {
          type: "chart",
          title: "数据分析图表",
          chartType: "bar",  // bar | line | pie | doughnut | area | radar
          chartData: {
            labels: ["Q1", "Q2", "Q3", "Q4"],
            datasets: [
              { name: "2024年", values: [100, 150, 200, 180] },
              { name: "2025年", values: [120, 180, 220, 250] }
            ]
          },
          chartOptions: {
            showLegend: true,
            showValue: true,
            showTitle: true
          }
        }
      ]
    }
  ]
};
```

---

## 第三阶段：PPT 生成

### 3.1 pptxgenjs 配置模板

**⚠️⚠️⚠️ 关键原则：所有配置必须从当前模板动态提取，绝不能写死！**

**不同的 PPT 模板会使用不同的字体、对齐方式、字号、颜色等，必须针对每个模板单独分析！**

```javascript
const pptxgen = require('pptxgenjs');
const pptx = new pptxgen();

// ============================================================
// ⚠️⚠️⚠️ 重要：以下所有配置都是占位符/示例！
// 实际使用时必须通过分析当前模板的 slide XML 获得精确值！
// 不同模板的字体、对齐、字号都不同，绝不能直接复制使用！
// ============================================================

const TEMPLATE_CONFIG = {
  // ⭐⭐⭐ 字体配置 - 必须从当前模板的 slide XML 精确提取！
  // 每个模板使用的字体都不同！必须从 <a:ea typeface="..."/> 提取！
  // 示例：腾讯模板用 "腾讯体 W7"，微软模板可能用 "微软雅黑"，其他模板可能用 "思源黑体" 等
  fonts: {
    // ⚠️ 以下是示例值，必须替换为当前模板实际使用的字体！
    title: '从slide1.xml提取的标题字体',    // 从 <a:ea typeface="..."/> 获取
    titleLatin: '从slide1.xml提取的西文字体', // 从 <a:latin typeface="..."/> 获取
    body: '从slide4.xml提取的正文字体',      // 从内容页提取
    bodyLatin: '从slide4.xml提取的西文字体',
    fallback: 'Microsoft YaHei'              // 备选字体
  },
  
  // 配色配置 (从 slide XML 和 theme1.xml 提取)
  colors: {
    primary: 'FFFFFF',      // 主要文字色
    secondary: 'E7E6E6',    // 次要文字色
    accent: '79E8F5',       // 强调色 (章节编号等)
    dark: '050E24',         // 深色/背景色
    // ⭐⭐⭐ 主题色映射 - 从 theme1.xml 的 <a:clrScheme> 提取
    // 用于将 schemeClr 转换为实际颜色
    schemeColors: {
      dk1: '000000',        // 深色1
      lt1: 'FFFFFF',        // 浅色1 (bg1)
      dk2: '0060FF',        // 深色2 (tx2) - 常用于背景
      lt2: 'FFFFFF',        // 浅色2
      accent1: '0060F0',    // 强调色1
      accent2: 'A736FF',    // 强调色2
      tx2: '0060FF'         // 文本2 - 注意这是常见的背景色引用！
    }
  },
  
  // ⭐⭐⭐ 背景配置 - 分页面类型配置！
  // 不同页面可能使用不同的背景类型（纯色/图片/渐变）
  backgrounds: {
    // 封面页背景 - 通常使用图片或复杂形状组合
    cover: {
      type: 'image',  // 'solid' | 'image' | 'gradient' | 'shapes'
      image: 'workspace/backgrounds/cover-bg.png',  // 如果是图片背景
      color: null,    // 如果是纯色背景
      // 装饰形状（如斜切遮罩）
      overlayShapes: [
        {
          type: 'custom',  // 自定义形状
          color: '0052D9',
          transparency: 50,
          // 形状路径点（从 <a:path> 提取）
        }
      ]
    },
    
    // 目录页背景 - 通常使用纯色 + 装饰形状
    toc: {
      type: 'solid',
      color: '0060FF',  // 从 schemeClr val="tx2" 映射得到
      // 或从 <p:bg><a:solidFill><a:schemeClr val="tx2"/> 提取后映射
      decorativeShapes: [
        { x: 1.22, y: 2.31, w: 1.31, h: 1.31, color: 'FFFFFF', transparency: 90 },
        { x: -0.01, y: 2.66, w: 2.17, h: 2.17, color: 'FFFFFF', transparency: 10 },
        { x: 1.95, y: 4.59, w: 0.58, h: 0.58, color: 'FFFFFF', transparency: 50 }
      ]
    },
    
    // 章节页背景 - 与目录页类似
    chapter: {
      type: 'solid',
      color: '0060FF',
      decorativeShapes: [
        // 同目录页的装饰形状
      ]
    },
    
    // 内容页背景 - 可能是纯色或图片
    content: {
      type: 'solid',
      color: '0060FF',
      // 如果有背景图，设置 type: 'image' 并指定路径
      image: null
    },
    
    // 结束页/感谢页背景
    thanks: {
      type: 'solid',
      color: '0060FF',
      decorativeShapes: [
        // 与目录页类似的装饰形状
      ]
    }
  },
  
  // ⚠️ 字号配置 - 必须从当前模板的 slide XML 精确提取!
  // 不同模板字号差异很大！必须从 sz="..." 属性获取！
  // sz="8000" -> 80pt, sz="4800" -> 48pt
  fontSizes: {
    // ⚠️ 以下是示例结构，必须替换为当前模板实际的字号！
    
    // 封面页 (从 slide1.xml 的 sz 属性提取)
    coverTitle: '从sz属性计算',      // sz值 ÷ 100
    coverSubtitle: '从sz属性计算',
    coverAuthor: '从sz属性计算',
    
    // 目录页 (从 slide2.xml 提取)
    // ⭐⭐⭐ 关键：必须区分"目录"标题和目录项的字号！
    tocHeading: '从sz属性计算',  // "目录"二字的字号（通常较大）
    tocItem: '从sz属性计算',     // 目录项的字号（通常较小，需精确提取！）
    
    // 章节页 (从 slide3.xml 提取)
    chapterNumber: '从sz属性计算',
    chapterTitle: '从sz属性计算',
    chapterSubtitle: '从sz属性计算',
    
    // 内容页 (从 slide4.xml 提取)
    pageSubtitle: '从sz属性计算',
    pageTitle: '从sz属性计算',
    sectionTitle: '从sz属性计算',
    bodyText: '从sz属性计算',
    
    // 感谢页
    thanks: '从sz属性计算'
  },
  
  // ⭐⭐⭐ 对齐方式配置 - 必须从当前模板的 slide XML 的 algn 属性提取!
  // 不同模板的对齐方式完全不同！有的居中，有的左对齐，有的右对齐！
  // 'left' | 'center' | 'right' | 'justify'
  alignments: {
    // ⚠️ 以下是示例结构，必须替换为当前模板实际的对齐方式！
    // 通过分析每页 XML 中的 algn="ctr"|"r"|"l" 属性获取
    
    // 封面页 (从 slide1.xml 的 algn 属性提取)
    coverTitle: '从slide1.xml提取',       // 分析 algn 属性
    coverSubtitle: '从slide1.xml提取',
    coverAuthor: '从slide1.xml提取',
    
    // 目录页 (从 slide2.xml 提取)
    tocHeading: '从slide2.xml提取',
    tocItem: '从slide2.xml提取',
    
    // 章节页 (从 slide3.xml 提取) - 不同模板差异很大！
    chapterNumber: '从slide3.xml提取',
    chapterTitle: '从slide3.xml提取',     // 可能是 center/left/right
    chapterSubtitle: '从slide3.xml提取',  // 可能是 center/left/right
    
    // 内容页 (从 slide4.xml 提取)
    pageSubtitle: '从slide4.xml提取',
    pageTitle: '从slide4.xml提取',
    sectionTitle: '从slide4.xml提取',
    bodyText: '从slide4.xml提取'
  },
  
  // ⚠️ 位置配置 - 必须从 slide XML 精确提取!
  // 使用百分比或英寸
  positions: {
    // 封面页位置 (从 slide1.xml 的 <a:off> 提取)
    cover: {
      title: { x: '26.5%', y: '31%', w: '70%', h: '15%' },
      subtitle: { x: '38.6%', y: '42%', w: '54%', h: '9%' },
      author: { x: '68.4%', y: '60%', w: '22%', h: '5%' },
      authorName: { x: '68.4%', y: '66%', w: '22%', h: '5%' }
    },
    
    // 目录页位置 (从 slide2.xml 提取)
    toc: {
      // 4列布局，起始位置和间距
      startX: 0.92,       // 第一列 x=4.6% -> 0.92 英寸
      colSpacing: 4.95,   // 列间距 (约24.7%)
      numberY: '57.2%',   // 编号 Y 位置
      titleY: '69.9%'     // 标题 Y 位置
    },
    
    // 章节页位置 (从 slide3.xml 提取)
    chapter: {
      number: { x: '43.2%', y: '14.3%', w: '30%', h: '30%' },
      title: { x: '15.2%', y: '35.7%', w: '70%', h: '15%' },
      subtitle: { x: '15.2%', y: '51.5%', w: '70%', h: '8%' }
    },
    
    // 内容页位置 (从 slide4.xml 提取)
    content: {
      subtitle: { x: '3.2%', y: '6.3%', w: '45%', h: '6%' },
      title: { x: '3.6%', y: '21.4%', w: '45%', h: '10%' },
      body: { x: '3.8%', y: '31.3%', w: '45%', h: '60%' },
      image: { x: '51.7%', y: '31.3%', w: '45%', h: '55%' }
    }
  }
};

// 设置 16:9 宽屏尺寸
pptx.defineLayout({ name: 'LAYOUT_WIDE', width: 20, height: 11.25 });
pptx.layout = 'LAYOUT_WIDE';
```

### 3.2 封面页生成函数

**⚠️ 注意：位置、字号、字体必须与模板完全匹配！**

**⚠️⚠️⚠️ 关键问题解决方案：**

#### 问题1：标题文字换行问题
pptxgenjs 默认会根据文本框宽度自动换行。解决方案：
1. **使用 `fit: 'shrink'`** - 自动缩小字号以适应宽度（推荐）
2. **使用足够大的宽度** - 确保文本框宽度足够容纳所有文字
3. **根据文字长度动态计算宽度** - 中文约每字符 0.7-1.0 × 字号(pt) / 72 英寸

```javascript
// ⭐ 计算文本所需宽度的辅助函数
function calculateTextWidth(text, fontSize, isChinese = true) {
  // 中文字符宽度约等于字号，英文约0.5倍
  const avgCharWidth = isChinese ? fontSize / 72 : fontSize / 72 * 0.5;
  let width = 0;
  for (const char of text) {
    width += /[\u4e00-\u9fa5]/.test(char) ? fontSize / 72 : fontSize / 72 * 0.5;
  }
  return width * 1.1; // 留10%余量
}
```

#### 问题2：封面页图片斜切布局（通用方案）

某些模板使用斜切（平行四边形）布局，需要分析模板的精确结构。

**⭐⭐⭐ 分析封面页布局的通用步骤：**

```
1. 解压模板，读取封面页 slide XML (通常是 slide1.xml)
2. 分析 <p:sp> 形状元素，识别：
   - 图片位置和尺寸 (<a:off>, <a:ext>)
   - 形状类型（矩形、自定义路径等）
   - 填充颜色和透明度 (<a:srgbClr>, <a:alpha>)
3. 确定层次结构（从下到上的渲染顺序）
4. 在 pptxgenjs 中按相同顺序重建
```

**⭐⭐⭐ 通用背景处理函数：**

根据模板分析结果，不同页面可能使用不同的背景类型。以下是通用的背景处理方案：

```javascript
// ⭐⭐⭐ 通用背景设置函数
function applySlideBackground(slide, pptx, bgConfig, colors) {
  /**
   * bgConfig 结构:
   * {
   *   type: 'solid' | 'image' | 'gradient' | 'shapes',
   *   color: '0060FF',           // 纯色背景颜色
   *   image: 'path/to/image',    // 图片路径
   *   decorativeShapes: [...],   // 装饰形状数组
   *   overlayShapes: [...]       // 遮罩层数组
   * }
   */
  
  // 1. 基础背景
  switch (bgConfig.type) {
    case 'image':
      if (bgConfig.image) {
        slide.background = { path: bgConfig.image };
      }
      break;
    
    case 'solid':
      slide.background = { color: bgConfig.color || colors.dark };
      break;
    
    case 'gradient':
      // pptxgenjs 背景不直接支持渐变，使用纯色基础 + 渐变形状
      slide.background = { color: bgConfig.baseColor || colors.dark };
      if (bgConfig.gradientStops) {
        slide.addShape(pptx.ShapeType.rect, {
          x: 0, y: 0, w: '100%', h: '100%',
          fill: {
            type: 'gradient',
            gradientType: 'linear',
            degrees: bgConfig.gradientAngle || 90,
            stops: bgConfig.gradientStops
          },
          line: { type: 'none' }
        });
      }
      break;
    
    case 'shapes':
    default:
      // 使用纯色基础
      slide.background = { color: bgConfig.color || colors.dark };
      break;
  }
  
  // 2. 添加遮罩层（如封面的半透明渐变层）
  if (bgConfig.overlayShapes && bgConfig.overlayShapes.length > 0) {
    bgConfig.overlayShapes.forEach(overlay => {
      slide.addShape(pptx.ShapeType.rect, {
        x: overlay.x, y: overlay.y,
        w: overlay.w, h: overlay.h,
        fill: { 
          type: 'solid',
          color: overlay.color,
          transparency: overlay.transparency || 0
        },
        line: { type: 'none' }
      });
    });
  }
  
  // 3. 添加装饰形状（如半透明方块）
  if (bgConfig.decorativeShapes && bgConfig.decorativeShapes.length > 0) {
    bgConfig.decorativeShapes.forEach(shape => {
      slide.addShape(pptx.ShapeType.rect, {
        x: shape.x, y: shape.y,
        w: shape.w, h: shape.h,
        fill: {
          type: 'solid',
          color: shape.color || colors.primary,
          transparency: shape.transparency || 0
        },
        line: { type: 'none' }
      });
    });
  }
}

// ⭐⭐⭐ 从主题色引用获取实际颜色
function resolveSchemeColor(schemeColorName, colors) {
  /**
   * 将 schemeClr 值转换为实际 RGB 颜色
   * 例如: 'tx2' -> '0060FF', 'bg1' -> 'FFFFFF'
   */
  const mapping = colors.schemeColors || {
    dk1: '000000',
    lt1: 'FFFFFF',
    dk2: '0060FF',
    lt2: 'FFFFFF',
    tx2: '0060FF',
    bg1: 'FFFFFF',
    accent1: '0060F0'
  };
  return mapping[schemeColorName] || schemeColorName;
}
```

**⭐ 通用渐变/斜切效果实现方案：**

由于 pptxgenjs 不直接支持自定义路径斜切，可使用以下方案模拟：

```javascript
function createCoverSlide(pptx, config, content) {
  const slide = pptx.addSlide();
  
  // ⭐⭐⭐ 使用通用背景处理函数
  applySlideBackground(slide, pptx, config.backgrounds.cover, config.colors);
  
  // 3. 文字元素使用足够宽度避免换行
  const pos = config.positions.cover;
  const titleWidth = calculateTextWidth(content.title, config.fontSizes.coverTitle);
  
  // 主标题 - 使用动态计算的宽度
  slide.addText(content.title, {
    x: pos.title.x, 
    y: pos.title.y, 
    w: Math.max(titleWidth, 8),  // ⭐ 确保宽度足够
    h: pos.title.h,
    fontSize: config.fontSizes.coverTitle,
    fontFace: config.fonts.title,
    color: config.colors.primary,
    bold: true,
    align: config.alignments.coverTitle || 'left',
    fit: 'shrink'  // ⭐ 如果仍然超出，自动缩小
  });
  
  // 副标题 - 48pt，前面有蓝色圆点装饰
  if (content.subtitle) {
    // 蓝色圆点装饰 (可选)
    slide.addShape('ellipse', {
      x: parseFloat(pos.subtitle.x) - 1.5 + '%',
      y: parseFloat(pos.subtitle.y) + 1.5 + '%',
      w: 0.25, h: 0.25,
      fill: { color: config.colors.accent }  // 79E8F5
    });
    
    slide.addText(content.subtitle, {
      x: pos.subtitle.x, y: pos.subtitle.y, w: pos.subtitle.w, h: pos.subtitle.h,
      fontSize: config.fontSizes.coverSubtitle,  // 48pt
      fontFace: config.fonts.title,
      color: config.colors.primary,
      bold: true,
      align: 'right'
    });
  }
  
  // 主讲人标签
  slide.addText('主讲人', {
    x: pos.author.x, y: pos.author.y, w: pos.author.w, h: pos.author.h,
    fontSize: config.fontSizes.coverAuthor,  // 28pt
    fontFace: config.fonts.body,
    color: config.colors.primary,
    align: 'right'
  });
  
  // 主讲人姓名
  slide.addText(content.author || '', {
    x: pos.authorName.x, y: pos.authorName.y, w: pos.authorName.w, h: pos.authorName.h,
    fontSize: config.fontSizes.coverAuthor,  // 28pt
    fontFace: config.fonts.body,
    color: config.colors.primary,
    align: 'right'
  });
  
  return slide;
}
```

### 3.3 目录页生成函数

**⚠️⚠️⚠️ 目录页字号必须从模板精确提取！不同模板差异很大！**

**⭐ 通用目录页分析步骤：**

```
1. 找到模板的目录页 slide XML（通常是 slide2.xml）
2. 分析所有文本元素的 sz 属性，区分：
   - "目录"标题字号（如果有）
   - 目录项字号（⭐⭐⭐ 关键！不同模板差异很大！）
3. 提取颜色、透明度（alpha）、位置等信息
4. 注意装饰元素（方块、线条等）
```

**⭐⭐⭐ 关键提醒：目录项字号通常比预期小很多！**

常见错误：假设目录项字号是 60pt 或 32pt，但实际可能只有 20-30pt。
必须从模板 XML 的 `sz` 属性精确提取！

```javascript
function createTOCSlide(pptx, config, chapters, currentIndex = 0) {
  const slide = pptx.addSlide();
  
  // ⭐⭐⭐ 使用通用背景处理函数
  applySlideBackground(slide, pptx, config.backgrounds.toc, config.colors);
  
  const pos = config.positions.toc;
  
  // ⭐ "目录"标题（如果模板有）- 从模板精确提取字号！
  if (config.showTocHeading !== false) {
    slide.addText('目录', {
      x: pos.heading?.x || 0.5,
      y: pos.heading?.y || 3,
      w: pos.heading?.w || 2,
      h: pos.heading?.h || 1,
      fontSize: config.fontSizes.tocHeading,  // ⭐ 从模板提取！
      fontFace: config.fonts.title,
      color: config.colors.tocHeading || config.colors.dark,
      bold: false
    });
  }
  
  // ⭐⭐⭐ 目录项列表 - 字号必须从模板精确提取！
  const tocItems = chapters.map((chapter, i) => ({
    text: `${i + 1}. ${chapter.title}`,
    options: {
      fontSize: config.fontSizes.tocItem,  // ⭐⭐⭐ 关键！从模板 sz 属性提取！
      fontFace: config.fonts.title,
      color: config.colors.tocItem || 'FFFFFF',
      // 非当前项使用透明度（如果模板有此效果）
      transparency: config.tocItemTransparency && i !== currentIndex 
        ? config.tocItemTransparency : 0,
      bullet: false,
      paraSpaceAfter: config.tocLineSpacing || 10
    }
  }));
  
  slide.addText(tocItems, {
    x: pos.items?.x || 4,
    y: pos.items?.y || 2,
    w: pos.items?.w || 6,
    h: pos.items?.h || 4,
    valign: pos.items?.valign || 'middle',
    lineSpacing: config.tocLineHeight || 40
  });
  
  // 装饰元素（如果模板有）- 位置从模板精确提取
  // ⭐⭐⭐ 注意：必须设置 line: 'none' 避免黑色边框！
  if (config.tocDecorations) {
    config.tocDecorations.forEach(dec => {
      slide.addShape(dec.type || 'rect', {
        x: dec.x, y: dec.y, w: dec.w, h: dec.h,
        fill: { color: dec.color, transparency: dec.transparency },
        line: 'none'  // ⭐ 必须无边框！
      });
    });
  }
  
  return slide;
}
```

### 3.4 章节页生成函数

**⚠️ 注意：章节标题和副标题必须使用模板的对齐方式（通常是居中）！**

```javascript
function createChapterSlide(pptx, config, chapter) {
  const slide = pptx.addSlide();
  
  // ⭐⭐⭐ 使用通用背景处理函数
  applySlideBackground(slide, pptx, config.backgrounds.chapter, config.colors);
  
  const pos = config.positions.chapter;
  
  // 大号章节编号 - 166pt 青色半透明
  slide.addText(chapter.number, {
    x: pos.number.x, y: pos.number.y, w: pos.number.w, h: pos.number.h,
    fontSize: config.fontSizes.chapterNumber,  // 166pt
    fontFace: config.fonts.title,
    color: config.colors.accent,               // 79E8F5
    transparency: 50,                          // 模拟渐变透明
    align: config.alignments.chapterNumber     // ⭐ 使用模板对齐方式
  });
  
  // 章节主标题 - 72pt 粗体白色 居中对齐
  slide.addText(chapter.title, {
    x: pos.title.x, y: pos.title.y, w: pos.title.w, h: pos.title.h,
    fontSize: config.fontSizes.chapterTitle,  // 72pt
    fontFace: config.fonts.title,
    color: config.colors.primary,
    bold: true,
    align: config.alignments.chapterTitle     // ⭐ 居中对齐 'center'
  });
  
  // 章节副标题 - 24pt 白色 居中对齐
  if (chapter.subtitle) {
    slide.addText(chapter.subtitle, {
      x: pos.subtitle.x, y: pos.subtitle.y, w: pos.subtitle.w, h: pos.subtitle.h,
      fontSize: config.fontSizes.chapterSubtitle,  // 24pt
      fontFace: config.fonts.body,
      color: config.colors.primary,
      align: config.alignments.chapterSubtitle  // ⭐ 居中对齐 'center'
    });
  }
  
  return slide;
}
```

### 3.5 内容页生成函数

**⭐⭐⭐ 关键：正文页背景可能是渐变，需要从模板精确提取！**

```javascript
function createContentSlide(pptx, config, page) {
  const slide = pptx.addSlide();
  
  // ⭐⭐⭐ 使用通用背景处理函数
  applySlideBackground(slide, pptx, config.backgrounds.content, config.colors);
    slide.addShape('rect', {
      x: 0, y: 0, w: '100%', h: '100%',
      fill: {
        type: 'gradient',
        gradientType: 'linear',
        rotate: grad.angle,
        stops: grad.stops.map(s => ({
          position: s.position,
          color: s.color,
          transparency: s.transparency || 0
        }))
      },
      line: 'none'  // ⭐ 必须无边框！
    });
  } else if (config.colors?.contentBackground) {
    // 方案3：纯色背景
    slide.background = { color: config.colors.contentBackground };
  }
  
  const pos = config.positions.content;
  
  // 页面小标题 - 32pt
  if (page.subtitle) {
    slide.addText(page.subtitle, {
      x: pos.subtitle.x, y: pos.subtitle.y, w: pos.subtitle.w, h: pos.subtitle.h,
      fontSize: config.fontSizes.pageSubtitle,  // 32pt
      fontFace: config.fonts.title,
      color: config.colors.primary,
      bold: true
    });
  }
  
  // 页面大标题 - 44pt
  slide.addText(page.title, {
    x: pos.title.x, y: pos.title.y, w: pos.title.w, h: pos.title.h,
    fontSize: config.fontSizes.pageTitle,  // 44pt
    fontFace: config.fonts.title,
    color: config.colors.primary,
    bold: true
  });
  
  // 内容区域
  if (page.sections && page.sections.length > 0) {
    const sectionCount = page.sections.length;
    const startY = 3.5;  // 英寸
    const availableHeight = 6.5;
    const sectionHeight = availableHeight / sectionCount;
    
    page.sections.forEach((section, i) => {
      const y = startY + i * sectionHeight;
      
      // 要点标题 - 28pt 粗体
      slide.addText(section.title, {
        x: '3.5%', y: y, w: '42%', h: 0.5,
        fontSize: config.fontSizes.sectionTitle,  // 28pt
        fontFace: config.fonts.title,
        color: config.colors.primary,
        bold: true
      });
      
      // 要点描述 - 16pt
      if (section.desc) {
        const descLines = Array.isArray(section.desc) ? section.desc : [section.desc];
        const descText = descLines.map(line => ({
          text: '• ' + line,
          options: { breakLine: true }
        }));
        
        slide.addText(descText, {
          x: '3.5%', y: y + 0.55, w: '42%', h: sectionHeight - 0.7,
          fontSize: config.fontSizes.bodyText,  // 16pt
          fontFace: config.fonts.body,
          color: config.colors.primary,
          valign: 'top',
          lineSpacingMultiple: 1.3
        });
      }
    });
  }
  
  // 右侧图片
  if (page.image) {
    slide.addImage({
      path: page.image,
      x: pos.image.x, y: pos.image.y,
      w: pos.image.w, h: pos.image.h,
      sizing: { type: 'contain', w: 9, h: 6.2 }
    });
  }
  
  // ⭐ 添加装饰元素（如果模板有）- 注意无边框！
  if (config.decorations?.content) {
    config.decorations.content.forEach(dec => {
      slide.addShape(dec.type || 'rect', {
        x: dec.x, y: dec.y, w: dec.w, h: dec.h,
        fill: { color: dec.fill.color, transparency: dec.fill.transparency || 0 },
        line: 'none'  // ⭐ 必须无边框！
      });
    });
  }
  
  return slide;
}
```

### 3.6 数据卡片页生成函数

**⭐ 注意：卡片背景形状必须正确处理边框！**

```javascript
function createDataCardsSlide(pptx, config, page) {
  const slide = pptx.addSlide();
  
  // ⭐ 背景处理（同内容页）
  if (config.backgrounds.content) {
    slide.background = { path: config.backgrounds.content };
  } else if (config.gradients?.content) {
    const grad = config.gradients.content;
    slide.addShape('rect', {
      x: 0, y: 0, w: '100%', h: '100%',
      fill: {
        type: 'gradient',
        gradientType: 'linear',
        rotate: grad.angle,
        stops: grad.stops
      },
      line: 'none'
    });
  }
  
  // 页面标题
  slide.addText(page.title, {
    x: '5%', y: '8%', w: '90%', h: '10%',
    fontSize: config.fontSizes.pageTitle,
    fontFace: config.fonts.title,
    color: config.colors.primary,
    bold: true,
    align: 'center'
  });
  
  const cards = page.dataCards || [];
  const cardCount = cards.length;
  const cardWidth = cardCount <= 2 ? 7 : (cardCount === 3 ? 5.5 : 4);
  const cardGap = cardCount <= 2 ? 2 : 0.8;
  const totalWidth = cardCount * cardWidth + (cardCount - 1) * cardGap;
  const startX = (20 - totalWidth) / 2;
  
  cards.forEach((card, i) => {
    const x = startX + i * (cardWidth + cardGap);
    
    // ⭐⭐⭐ 卡片背景 - 必须正确处理边框！
    slide.addShape('rect', {
      x: x, y: 3, w: cardWidth, h: 5.5,
      fill: { type: 'solid', color: 'FFFFFF', transparency: 92 },
      // ⭐ 如果模板卡片有边框，从模板提取颜色和宽度
      // ⭐ 如果模板卡片无边框，使用 line: 'none'
      line: config.cardBorder || 'none',  // ⭐ 默认无边框！
      rectRadius: 0.1
    });
    
    // 大号数字
    slide.addText(card.value, {
      x: x + 0.3, y: 3.5, w: cardWidth - 0.6, h: 1.8,
      fontSize: 64,
      fontFace: config.fonts.title,
      color: config.colors.accent,
      bold: true,
      align: 'center'
    });
    
    // 数据后缀
    if (card.suffix) {
      slide.addText(card.suffix, {
        x: x + 0.3, y: 5.2, w: cardWidth - 0.6, h: 0.8,
        fontSize: 24,
        fontFace: config.fonts.title,
        color: config.colors.primary,
        bold: true,
        align: 'center'
      });
    }
    
    // 数据说明
    slide.addText(card.label, {
      x: x + 0.3, y: 6.1, w: cardWidth - 0.6, h: 1.8,
      fontSize: 16,
      fontFace: config.fonts.body,
      color: config.colors.primary,
      align: 'center',
      valign: 'top',
      wrap: true
    });
  });
  
  return slide;
}
```

### 3.7 图表页生成函数

**⭐⭐⭐ pptxgenjs 内置强大的图表功能，支持多种图表类型！**

#### 支持的图表类型

| 类型 | pptxgenjs 常量 | 说明 |
|------|---------------|------|
| 柱状图 | `pptx.ChartType.bar` | 垂直柱状图 |
| 横向柱状图 | `pptx.ChartType.bar3D` | 3D 柱状图 |
| 折线图 | `pptx.ChartType.line` | 折线趋势图 |
| 面积图 | `pptx.ChartType.area` | 面积趋势图 |
| 饼图 | `pptx.ChartType.pie` | 饼状图 |
| 圆环图 | `pptx.ChartType.doughnut` | 环形图 |
| 雷达图 | `pptx.ChartType.radar` | 雷达/蜘蛛图 |
| 散点图 | `pptx.ChartType.scatter` | 散点图 |

#### 图表数据结构

```javascript
// ⭐ 图表数据的标准格式
const chart

…(truncated)
