---
name: universal-pptx-generator
description: 通用 PPT 生成技能。能够根据用户指定的任意 PPT 模板,结合提供的图文素材(文档、图片等),自动生成一份风格统一的演示文稿。支持自动分析模板结构、提取背景图/背景色、解析配色方案和字体规范,然后根据素材内容生成完整 PPT。支持多种图表类型(柱状图、折线图、饼图、雷达图等)的数据可视化展示。关键词:PPT生成、模板分析、演示文稿、幻灯片、图文排版、自动化、图表、数据可视化。
---
# 通用 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
```
**pptxgenjs 对应代码:**
```javascript
slide.background = { color: '0D1E43' }; // 深蓝色
```
#### 2. 背景图片 (BlipFill)
```xml
```
**pptxgenjs 对应代码:**
```javascript
slide.background = { path: 'workspace/backgrounds/cover-bg.png' };
```
#### 3. 渐变背景 (GradFill)
```xml
```
**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. 检查是否有 背景定义
bg_match = re.search(r'(.*?)', content, re.DOTALL)
if bg_match:
bg_content = bg_match.group(1)
# 纯色背景
if '' 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 '' in bg_content:
result['background_type'] = 'gradient'
# 图片背景
elif '' in bg_content:
result['background_type'] = 'image'
embed = re.search(r'r:embed="([^"]+)"', bg_content)
if embed:
result['background_image'] = embed.group(1)
# 2. 检查是否有全屏图片作为背景(无 时)
if not result['background_type']:
pics = re.findall(r'(.*?)', content, re.DOTALL)
for pic in pics:
# 检查是否是大尺寸图片(可能是背景图)
ext_match = re.search(r'', 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'(.*?)', content, re.DOTALL)
for shape in shapes:
# 检查是否有透明度设置
alpha = re.search(r' 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. **精确位置** - `` 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'([^<]+)', 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'', 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'', 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']*>(.*?)', content, re.DOTALL)
for i, ln in enumerate(lns):
if '' in ln:
print(f'边框{i}: 无边框 (noFill)')
elif '' 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']*>(.*?)', content, re.DOTALL)
for i, grad in enumerate(gradFills):
# 提取角度
lin = re.search(r'(.*?)', 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'', 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'(.*?)', content, re.DOTALL)
for i, sp in enumerate(sps):
texts = re.findall(r'([^<]+)', 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 = '' 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
```
### 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']*>([^<]+)', 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 精确提取!
// 每个模板使用的字体都不同!必须从 提取!
// 示例:腾讯模板用 "腾讯体 W7",微软模板可能用 "微软雅黑",其他模板可能用 "思源黑体" 等
fonts: {
// ⚠️ 以下是示例值,必须替换为当前模板实际使用的字体!
title: '从slide1.xml提取的标题字体', // 从 获取
titleLatin: '从slide1.xml提取的西文字体', // 从 获取
body: '从slide4.xml提取的正文字体', // 从内容页提取
bodyLatin: '从slide4.xml提取的西文字体',
fallback: 'Microsoft YaHei' // 备选字体
},
// 配色配置 (从 slide XML 和 theme1.xml 提取)
colors: {
primary: 'FFFFFF', // 主要文字色
secondary: 'E7E6E6', // 次要文字色
accent: '79E8F5', // 强调色 (章节编号等)
dark: '050E24', // 深色/背景色
// ⭐⭐⭐ 主题色映射 - 从 theme1.xml 的 提取
// 用于将 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,
// 形状路径点(从 提取)
}
]
},
// 目录页背景 - 通常使用纯色 + 装饰形状
toc: {
type: 'solid',
color: '0060FF', // 从 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 的 提取)
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. 分析 形状元素,识别:
- 图片位置和尺寸 (, )
- 形状类型(矩形、自定义路径等)
- 填充颜色和透明度 (, )
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 chartData = [
{
name: "系列1",
labels: ["类别A", "类别B", "类别C", "类别D"],
values: [100, 200, 300, 400]
},
{
name: "系列2",
labels: ["类别A", "类别B", "类别C", "类别D"],
values: [150, 250, 350, 450]
}
];
```
#### 图表页生成函数
```javascript
function createChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
// 背景
if (config.backgrounds.content) {
slide.background = { path: config.backgrounds.content };
}
// 页面标题
slide.addText(page.title, {
x: '5%', y: '5%', w: '90%', h: '10%',
fontSize: config.fontSizes.pageTitle || 36,
fontFace: config.fonts.title,
color: config.colors.primary,
bold: true,
align: 'center'
});
// ⭐ 根据图表类型选择
const chartTypeMap = {
'bar': pptx.ChartType.bar,
'bar3d': pptx.ChartType.bar3D,
'line': pptx.ChartType.line,
'area': pptx.ChartType.area,
'pie': pptx.ChartType.pie,
'doughnut': pptx.ChartType.doughnut,
'radar': pptx.ChartType.radar,
'scatter': pptx.ChartType.scatter
};
const chartType = chartTypeMap[page.chartType] || pptx.ChartType.bar;
// ⭐ 转换数据格式
const chartData = page.chartData.datasets.map(ds => ({
name: ds.name,
labels: page.chartData.labels,
values: ds.values
}));
// ⭐ 图表配置选项
const chartOptions = {
x: 1, y: 2, w: 18, h: 8, // 位置和尺寸
// 标题
showTitle: page.chartOptions?.showTitle || false,
title: page.chartOptions?.chartTitle || page.title,
titleFontFace: config.fonts.title,
titleFontSize: 18,
titleColor: config.colors.primary,
// 图例
showLegend: page.chartOptions?.showLegend !== false,
legendPos: page.chartOptions?.legendPos || 'b', // 't', 'b', 'l', 'r', 'tr'
legendFontFace: config.fonts.body,
legendFontSize: 12,
legendColor: config.colors.primary,
// 数据标签
showValue: page.chartOptions?.showValue || false,
dataLabelPosition: page.chartOptions?.labelPos || 'outEnd', // 'outEnd', 'inEnd', 'ctr', 'inBase'
dataLabelFontFace: config.fonts.body,
dataLabelFontSize: 10,
dataLabelColor: config.colors.primary,
// 配色
chartColors: page.chartOptions?.colors || [
config.colors.accent || '79E8F5',
config.colors.secondary || 'E7E6E6',
'0052D9', 'FF6B6B', '4ECDC4', 'FFE66D', '95E1D3', 'F38181'
],
// 背景和边框
fill: page.chartOptions?.fill || 'FFFFFF',
border: page.chartOptions?.border || { pt: 0, color: 'FFFFFF' },
// 网格线
catGridLine: { style: 'none' },
valGridLine: {
style: page.chartOptions?.showGridLine !== false ? 'solid' : 'none',
color: 'E0E0E0',
size: 0.5
}
};
// ⭐⭐⭐ 添加图表
slide.addChart(chartType, chartData, chartOptions);
// 添加图表说明(如果有)
if (page.chartDescription) {
slide.addText(page.chartDescription, {
x: '5%', y: '92%', w: '90%', h: '6%',
fontSize: 12,
fontFace: config.fonts.body,
color: config.colors.secondary || 'AAAAAA',
align: 'center'
});
}
return slide;
}
```
#### 柱状图/条形图示例
```javascript
// ⭐ 基础柱状图
function createBarChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
slide.background = { path: config.backgrounds.content };
const chartData = [
{
name: "销售额",
labels: ["一月", "二月", "三月", "四月", "五月", "六月"],
values: [120, 180, 150, 200, 250, 220]
}
];
slide.addChart(pptx.ChartType.bar, chartData, {
x: 1, y: 2, w: 18, h: 8,
showLegend: true,
legendPos: 'b',
showValue: true,
dataLabelPosition: 'outEnd',
chartColors: [config.colors.accent],
barGapWidthPct: 50, // 柱子间距
// 3D 效果(可选)
// bar3DShape: 'cylinder',
// shadow: { type: 'outer', blur: 3, offset: 3, angle: 45, opacity: 0.4 }
});
return slide;
}
// ⭐ 多系列柱状图(对比图)
function createGroupedBarChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{
name: "2024年",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [100, 150, 200, 180]
},
{
name: "2025年",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [120, 180, 220, 250]
}
];
slide.addChart(pptx.ChartType.bar, chartData, {
x: 1, y: 2, w: 18, h: 8,
showLegend: true,
showValue: true,
chartColors: ['79E8F5', '0052D9'],
barGrouping: 'clustered' // 'clustered' 分组, 'stacked' 堆叠, 'percentStacked' 百分比堆叠
});
return slide;
}
// ⭐ 堆叠柱状图
function createStackedBarChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{ name: "产品A", labels: ["Q1", "Q2", "Q3", "Q4"], values: [50, 60, 70, 80] },
{ name: "产品B", labels: ["Q1", "Q2", "Q3", "Q4"], values: [30, 40, 50, 60] },
{ name: "产品C", labels: ["Q1", "Q2", "Q3", "Q4"], values: [20, 30, 40, 50] }
];
slide.addChart(pptx.ChartType.bar, chartData, {
x: 1, y: 2, w: 18, h: 8,
barGrouping: 'stacked', // ⭐ 堆叠模式
showLegend: true,
showValue: true,
chartColors: ['79E8F5', '0052D9', 'FF6B6B']
});
return slide;
}
```
#### 折线图/面积图示例
```javascript
// ⭐ 折线图(趋势分析)
function createLineChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{
name: "用户增长",
labels: ["1月", "2月", "3月", "4月", "5月", "6月"],
values: [1000, 1500, 2200, 3100, 4500, 6800]
},
{
name: "活跃用户",
labels: ["1月", "2月", "3月", "4月", "5月", "6月"],
values: [800, 1200, 1800, 2600, 3800, 5500]
}
];
slide.addChart(pptx.ChartType.line, chartData, {
x: 1, y: 2, w: 18, h: 8,
showLegend: true,
legendPos: 'r', // 右侧图例
// 线条样式
lineSize: 2, // 线条粗细
lineSmooth: true, // 平滑曲线
// 数据点标记
lineDataSymbol: 'circle', // 'circle', 'dash', 'diamond', 'dot', 'none', 'square', 'triangle'
lineDataSymbolSize: 8,
// 显示数据值
showValue: true,
dataLabelPosition: 'outEnd',
chartColors: ['79E8F5', '0052D9']
});
return slide;
}
// ⭐ 面积图(带填充)
function createAreaChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{
name: "收入",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [100, 200, 300, 400]
}
];
slide.addChart(pptx.ChartType.area, chartData, {
x: 1, y: 2, w: 18, h: 8,
showLegend: true,
chartColors: ['79E8F5'],
// 透明度
chartColorsOpacity: 50 // 50% 透明
});
return slide;
}
```
#### 饼图/圆环图示例
```javascript
// ⭐ 饼图(占比分析)
function createPieChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
slide.background = { path: config.backgrounds.content };
// 标题
slide.addText(page.title || '市场份额分析', {
x: '5%', y: '5%', w: '90%', h: '10%',
fontSize: 36,
fontFace: config.fonts.title,
color: config.colors.primary,
bold: true,
align: 'center'
});
const chartData = [
{
name: "市场份额",
labels: ["产品A", "产品B", "产品C", "其他"],
values: [45, 25, 20, 10]
}
];
slide.addChart(pptx.ChartType.pie, chartData, {
x: 4, y: 2.5, w: 12, h: 7, // 居中放置
showLegend: true,
legendPos: 'r',
// 显示百分比
showValue: true,
showPercent: true, // ⭐ 显示百分比
showLabel: true, // 显示标签
// 饼图特有选项
firstSliceAng: 0, // 第一块起始角度
chartColors: ['79E8F5', '0052D9', 'FF6B6B', 'E7E6E6']
});
return slide;
}
// ⭐ 圆环图(带中心说明)
function createDoughnutChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{
name: "项目进度",
labels: ["已完成", "进行中", "未开始"],
values: [65, 25, 10]
}
];
slide.addChart(pptx.ChartType.doughnut, chartData, {
x: 4, y: 2.5, w: 12, h: 7,
showLegend: true,
legendPos: 'b',
showValue: true,
showPercent: true,
// 圆环特有选项
holeSize: 50, // ⭐ 中心空洞大小(百分比)
chartColors: ['4ECDC4', 'FFE66D', 'FF6B6B']
});
// ⭐ 在圆环中心添加说明文字
slide.addText('65%\n完成率', {
x: 7.5, y: 5, w: 5, h: 1.5,
fontSize: 32,
fontFace: config.fonts.title,
color: config.colors.primary,
bold: true,
align: 'center',
valign: 'middle'
});
return slide;
}
```
#### 雷达图示例
```javascript
// ⭐ 雷达图(多维度对比)
function createRadarChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
const chartData = [
{
name: "产品A",
labels: ["性能", "易用性", "稳定性", "安全性", "可扩展性"],
values: [90, 75, 85, 80, 70]
},
{
name: "产品B",
labels: ["性能", "易用性", "稳定性", "安全性", "可扩展性"],
values: [70, 90, 75, 85, 90]
}
];
slide.addChart(pptx.ChartType.radar, chartData, {
x: 3, y: 2, w: 14, h: 8,
showLegend: true,
legendPos: 'b',
// 雷达图特有选项
radarStyle: 'standard', // 'standard' 或 'marker' 或 'filled'
chartColors: ['79E8F5', '0052D9']
});
return slide;
}
```
#### 组合图表示例
```javascript
// ⭐ 柱状图+折线图组合(双坐标轴)
function createComboChartSlide(pptx, config, page) {
const slide = pptx.addSlide();
// 销售额数据(柱状图)
const barData = [
{
name: "销售额(万)",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [100, 150, 200, 180]
}
];
// 增长率数据(折线图)
const lineData = [
{
name: "增长率(%)",
labels: ["Q1", "Q2", "Q3", "Q4"],
values: [10, 50, 33, -10]
}
];
// ⭐ 使用 addChart 的组合模式
slide.addChart(
[pptx.ChartType.bar, pptx.ChartType.line],
[barData, lineData],
{
x: 1, y: 2, w: 18, h: 8,
showLegend: true,
// 双坐标轴
catAxisTitle: '季度',
valAxisTitle: '销售额',
secondaryValAxis: true, // ⭐ 启用第二纵坐标轴
secondaryValAxisTitle: '增长率',
chartColors: ['79E8F5', 'FF6B6B']
}
);
return slide;
}
```
#### 图表样式美化
```javascript
// ⭐⭐⭐ 通用图表美化配置
const chartStyleOptions = {
// 标题样式
showTitle: true,
title: '图表标题',
titleFontFace: '腾讯体 W7',
titleFontSize: 18,
titleColor: 'FFFFFF',
titlePos: { x: 0, y: 0 },
// 图例样式
showLegend: true,
legendPos: 'b', // 底部
legendFontFace: '方正兰亭黑简体',
legendFontSize: 12,
legendColor: 'FFFFFF',
// 坐标轴样式
catAxisTitle: 'X轴标题',
valAxisTitle: 'Y轴标题',
catAxisLabelColor: 'FFFFFF',
valAxisLabelColor: 'FFFFFF',
catAxisLabelFontSize: 11,
valAxisLabelFontSize: 11,
catAxisLineShow: true,
valAxisLineShow: false,
// 网格线
catGridLine: { style: 'none' },
valGridLine: {
style: 'dash', // 'solid', 'dash', 'dot', 'none'
color: 'FFFFFF',
size: 0.5
},
// 数据标签
showValue: true,
dataLabelPosition: 'outEnd',
dataLabelFontFace: '方正兰亭黑简体',
dataLabelFontSize: 10,
dataLabelColor: 'FFFFFF',
dataLabelFontBold: false,
// 图表区域
fill: 'TRANSPARENT', // 透明背景
border: { pt: 0, color: 'FFFFFF' },
// 阴影效果
shadow: {
type: 'outer',
blur: 3,
offset: 2,
angle: 45,
opacity: 0.3
}
};
```
#### 从数据自动生成图表
```javascript
// ⭐ 智能图表类型推荐
function recommendChartType(data) {
const seriesCount = data.datasets?.length || 1;
const labelCount = data.labels?.length || 0;
const isTimeSeries = data.labels?.some(l => /^\d{4}|[Q季月]/.test(l));
const isPercentage = data.datasets?.every(ds =>
ds.values.reduce((a, b) => a + b, 0) <= 100
);
// 占比数据 -> 饼图/圆环图
if (isPercentage && labelCount <= 6) {
return labelCount <= 4 ? 'pie' : 'doughnut';
}
// 时间序列 -> 折线图
if (isTimeSeries) {
return 'line';
}
// 多系列对比 -> 柱状图
if (seriesCount > 1 && labelCount <= 10) {
return 'bar';
}
// 多维度评估 -> 雷达图
if (labelCount >= 5 && labelCount <= 8 && seriesCount <= 3) {
return 'radar';
}
// 默认柱状图
return 'bar';
}
// ⭐ 根据数据自动生成图表页
function createAutoChartSlide(pptx, config, page) {
const chartType = page.chartType || recommendChartType(page.chartData);
// 根据推荐类型调用对应函数
switch (chartType) {
case 'pie':
case 'doughnut':
return createPieChartSlide(pptx, config, { ...page, chartType });
case 'line':
case 'area':
return createLineChartSlide(pptx, config, { ...page, chartType });
case 'radar':
return createRadarChartSlide(pptx, config, { ...page, chartType });
default:
return createBarChartSlide(pptx, config, { ...page, chartType });
}
}
```
#### 表格展示
```javascript
// ⭐ 数据表格(配合图表使用)
function addDataTable(slide, config, tableData, options = {}) {
const {
x = 1, y = 8, w = 18, h = 2,
headerColor = config.colors.accent,
headerTextColor = config.colors.dark,
bodyColor = 'FFFFFF',
bodyTextColor = config.colors.dark,
fontSize = 12
} = options;
// 表头
const header = tableData.headers.map(h => ({
text: h,
options: {
fill: headerColor,
color: headerTextColor,
bold: true,
align: 'center',
fontFace: config.fonts.title
}
}));
// 表格数据
const rows = [header];
tableData.rows.forEach((row, i) => {
rows.push(row.map(cell => ({
text: String(cell),
options: {
fill: i % 2 === 0 ? bodyColor : 'F5F5F5',
color: bodyTextColor,
align: 'center',
fontFace: config.fonts.body
}
})));
});
slide.addTable(rows, {
x, y, w, h,
fontSize,
border: { pt: 0.5, color: 'E0E0E0' },
colW: Array(tableData.headers.length).fill(w / tableData.headers.length)
});
}
// ⭐ 图表+表格组合页
function createChartWithTableSlide(pptx, config, page) {
const slide = pptx.addSlide();
slide.background = { path: config.backgrounds.content };
// 标题
slide.addText(page.title, {
x: '5%', y: '3%', w: '90%', h: '8%',
fontSize: 32,
fontFace: config.fonts.title,
color: config.colors.primary,
bold: true,
align: 'center'
});
// 图表(上半部分)
const chartData = page.chartData.datasets.map(ds => ({
name: ds.name,
labels: page.chartData.labels,
values: ds.values
}));
slide.addChart(pptx.ChartType.bar, chartData, {
x: 1, y: 1.5, w: 18, h: 5.5,
showLegend: true,
showValue: true,
chartColors: ['79E8F5', '0052D9', 'FF6B6B']
});
// 表格(下半部分)
const tableData = {
headers: ['指标', ...page.chartData.labels],
rows: page.chartData.datasets.map(ds => [ds.name, ...ds.values])
};
addDataTable(slide, config, tableData, {
x: 1, y: 7.5, w: 18, h: 2.5
});
return slide;
}
```
### 3.8 感谢页生成函数
```javascript
function createThanksSlide(pptx, config) {
const slide = pptx.addSlide();
// ⭐⭐⭐ 使用通用背景处理函数
applySlideBackground(slide, pptx, config.backgrounds.thanks, config.colors);
slide.addText('THANKS', {
x: '5%', y: '40%', w: '50%', h: '20%',
fontSize: config.fontSizes.thanks,
fontFace: config.fonts.title,
color: config.colors.primary,
bold: true
});
return slide;
}
```
### 3.9 完整生成流程
```javascript
async function generatePPT(config, content) {
const pptx = new pptxgen();
pptx.defineLayout({ name: 'LAYOUT_WIDE', width: 20, height: 11.25 });
pptx.layout = 'LAYOUT_WIDE';
// 1. 封面页
createCoverSlide(pptx, config, content);
// 2. 目录页
createTOCSlide(pptx, config, content.chapters);
// 3. 各章节
content.chapters.forEach(chapter => {
// 章节页
createChapterSlide(pptx, config, chapter);
// 内容页
chapter.pages.forEach(page => {
switch(page.type) {
case 'dataCards':
createDataCardsSlide(pptx, config, page);
break;
case 'textOnly':
createTextOnlySlide(pptx, config, page);
break;
case 'chart':
createChartSlide(pptx, config, page);
break;
case 'chartWithTable':
createChartWithTableSlide(pptx, config, page);
break;
default:
createContentSlide(pptx, config, page);
}
});
});
// 4. 感谢页
createThanksSlide(pptx, config);
// 5. 保存
await pptx.writeFile({ fileName: 'output.pptx' });
}
```
---
## 第四阶段:清理临时文件
```bash
# 清理所有临时文件
rm -rf workspace/template-analysis
rm -rf workspace/docx-extract
rm -rf workspace/backgrounds
rm -rf workspace/images
rm -rf workspace/node_modules
rm -f workspace/package*.json
rm -f workspace/create-pptx.js
# 只保留最终 PPT
ls workspace/*.pptx
```
---
## 关键要点总结
### ⚠️ 必须遵守的原则
**⭐⭐⭐ 核心原则:所有配置必须从当前模板动态提取,绝不能写死或复制其他模板的值!**
1. **精确提取参数** - 所有字号、位置、颜色必须从 slide XML 精确提取
2. **精确提取字体** - ⭐⭐⭐ **字体名称必须从 `` 和 `` 精确提取,不同模板字体完全不同!**
3. **精确提取对齐方式** - ⭐⭐ **对齐方式必须从 `algn` 属性提取,不同模板的对齐设置差异很大!**
4. **不要添加额外元素** - 如果模板目录页没有"目录"标题,就不要添加
5. **保持布局一致** - 位置和尺寸必须与模板匹配
6. **单位换算正确** - sz ÷ 100 = pt, EMU ÷ 914400 = inch
### 常见错误避免
| 错误 | 正确做法 |
|------|----------|
| 使用推测的字号 | 从 XML 的 `sz` 属性精确提取 |
| 使用推测的位置 | 从 XML 的 `` 精确提取 |
| 添加模板没有的元素 | 只复制模板实际有的元素 |
| 使用错误的颜色 | 从 XML 的 `srgbClr` 精确提取 |
| **使用默认字体(如等线)** | **⭐ 从 XML 精确提取字体名称** |
| **使用错误的对齐方式** | **⭐ 从 XML 的 `algn` 属性提取** |
| **复制其他模板的配置** | **⭐⭐⭐ 每个模板必须单独分析!** |
| **文本框宽度不足导致换行** | **⭐ 使用动态宽度计算或 `fit: 'shrink'`** |
| **封面图片布局不正确** | **⭐ 分析模板的斜切/遮罩层次结构** |
| **形状出现黑色边框** | **⭐⭐⭐ 必须设置 `line: { color: 'FFFFFF', width: 0 }` 或 `line: 'none'`** |
| **背景渐变方向/颜色错误** | **⭐ 从 `` 精确提取渐变参数** |
### ⭐⭐⭐ 文本宽度处理(防止换行)
**问题:** 标题文字超出文本框宽度时会自动换行,破坏布局。
**解决方案 1:动态计算宽度**
```javascript
// 根据文字内容动态计算所需宽度
function calculateTextWidth(text, fontSize) {
let width = 0;
for (const char of text) {
// 中文字符宽度约等于字号,英文约0.5倍
width += /[\u4e00-\u9fa5]/.test(char) ? fontSize / 72 : fontSize / 72 * 0.5;
}
return width * 1.15; // 留15%余量
}
// 使用
const titleWidth = calculateTextWidth(content.title, 66);
slide.addText(content.title, {
w: Math.max(titleWidth, 8), // 确保最小宽度
// ...
});
```
**解决方案 2:使用 fit 选项**
```javascript
slide.addText(content.title, {
w: 10, h: 1.5,
fit: 'shrink', // 自动缩小字号以适应宽度
// ...
});
```
**解决方案 3:使用 autoFit 选项(推荐)**
```javascript
slide.addText(content.title, {
w: '80%', // 使用较大的百分比宽度
autoFit: true, // 自动调整
// ...
});
```
### ⭐⭐⭐ 封面页斜切布局处理(通用方案)
**问题:** 某些模板使用斜切(平行四边形)图片和遮罩布局。
**⭐ 通用分析步骤:**
```
1. 解压模板,读取封面页 slide XML
2. 识别所有 形状元素及其层次顺序
3. 分析每个形状的:
- 类型: 或
- 位置:
- 尺寸:
- 填充: +
- 透明度: (50000 = 50%)
4. 按顺序重建层次结构
```
**⭐ pptxgenjs 实现方案(通用框架):**
```javascript
function createCoverSlide(pptx, config, content) {
const slide = pptx.addSlide();
// 1. 背景(从模板提取)
slide.background = { color: config.colors.dark };
// 2. 图片层(如果模板有)- 位置从模板精确提取!
if (config.coverLayout?.photo) {
const photo = config.coverLayout.photo;
slide.addImage({
path: config.backgrounds.coverPhoto,
x: photo.x, y: photo.y, w: photo.w, h: photo.h
});
}
// 3. 遮罩层(如果模板有)- 参数从模板精确提取!
// pptxgenjs 不支持自定义路径,使用多层矩形模拟
if (config.coverLayout?.overlays) {
config.coverLayout.overlays.forEach(overlay => {
slide.addShape('rect', {
x: overlay.x, y: overlay.y, w: overlay.w, h: overlay.h,
fill: { color: overlay.color, transparency: overlay.transparency || 0 },
line: { width: 0 }
});
});
}
// 4. 文字元素...
}
```
**关键点:**
- 图片位置从模板 XML 的 `` 和 `` 精确提取
- 斜切角度从模板的 `` 路径计算
- 遮罩透明度从 `` 提取(50000 = 50%)
### 字体提取关键点
**⚠️ pptxgenjs 会使用默认字体(如"等线"),必须显式指定模板字体!**
```xml
```
**提取字体的 Python 命令:**
```bash
cat ppt/slides/slide1.xml | python3 -c "
import sys, re
content = sys.stdin.read()
print('Latin字体:', set(re.findall(r'latin typeface=\"([^\"]+)\"', content)))
print('EA字体:', set(re.findall(r'ea typeface=\"([^\"]+)\"', content)))
"
```
**在 pptxgenjs 中正确使用字体:**
```javascript
// ⭐ 必须使用模板提取的字体名称
slide.addText('标题文本', {
fontFace: '腾讯体 W7', // 从模板提取的实际字体
fontSize: 80,
// ...
});
```
### 单位换算公式
```
字号: sz 值 ÷ 100 = pt
位置/尺寸: EMU ÷ 914400 = inch
百分比: inch ÷ 画布尺寸 × 100%
画布尺寸 (16:9): 20 × 11.25 inch
```
### 字体/对齐映射表模板(每次分析模板后填写)
**⚠️⚠️⚠️ 以下是空白模板!每次分析新模板时,必须填写该模板的实际值!**
**不同模板的字体、对齐方式完全不同,绝不能复制其他模板的配置!**
| 元素类型 | EA字体(从XML提取) | Latin字体(从XML提取) | 字号(从sz计算) | 对齐(从algn提取) |
|----------|------------------|---------------------|---------------|-----------------|
| 封面主标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 封面副标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 目录编号 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 目录标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 章节编号 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 章节标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 章节副标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 内容大标题 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
| 内容正文 | `<填写>` | `<填写>` | `<填写>pt` | `<填写>` |
**示例(某腾讯模板分析结果,仅供参考格式):**
| 元素类型 | EA字体 | Latin字体 | 字号 | 对齐 |
|----------|--------|-----------|------|------|
| 封面主标题 | 腾讯体 W7 | Calibri | 80pt | right |
| 章节标题 | 腾讯体 W7 | Calibri | 72pt | center |
| 内容正文 | 方正兰亭黑简体 | 方正兰亭黑简体 | 16pt | left |
**⚠️ 上面的示例值只是参考格式,实际使用时必须分析当前模板获取!**
### 对齐方式映射
| XML algn 值 | pptxgenjs align 值 | 说明 |
|-------------|-------------------|------|
| 无属性 | 'left' | 默认左对齐 |
| l | 'left' | 左对齐 |
| ctr | 'center' | 居中对齐 |
| r | 'right' | 右对齐 |
| just | 'justify' | 两端对齐 |
### 模板分析脚本模板
```python
#!/usr/bin/env python3
import re
import sys
def analyze_slide(content):
"""分析单页幻灯片的精确参数"""
result = {
'texts': [],
'sizes': {},
'colors': set(),
'positions': []
}
# 提取文本
result['texts'] = [t for t in re.findall(r'([^<]+)', content) if t.strip()]
# 提取字号
for s in set(re.findall(r'sz="(\d+)"', content)):
result['sizes'][s] = f'{int(s)/100}pt'
# 提取颜色
result['colors'] = set(re.findall(r'srgbClr val="([^"]+)"', content))
# 提取位置
for x, y in re.findall(r'', content):
x_pct = int(x) / 914400 / 20 * 100
y_pct = int(y) / 914400 / 11.25 * 100
result['positions'].append({'x': f'{x_pct:.1f}%', 'y': f'{y_pct:.1f}%'})
return result
# 使用示例
with open('workspace/template-analysis/ppt/slides/slide1.xml', 'r') as f:
result = analyze_slide(f.read())
print(result)
```
---
## ⭐⭐⭐ 关键细节:形状边框和背景渐变处理
### 形状边框问题(黑色边框修复)
**问题现象:** 模板中的形状是无边框的,但生成的PPT中出现了黑色边框。
**原因分析:**
模板 XML 中形状的边框定义方式:
```xml
```
**⭐⭐⭐ pptxgenjs 解决方案:**
```javascript
// ❌ 错误:会产生默认黑色边框
slide.addShape('rect', {
x: 1, y: 2, w: 3, h: 4,
fill: { color: 'FFFFFF', transparency: 90 }
// 没有指定 line,默认会有黑色边框!
});
// ✅ 正确方案1:显式设置 line 为透明或同色
slide.addShape('rect', {
x: 1, y: 2, w: 3, h: 4,
fill: { color: 'FFFFFF', transparency: 90 },
line: { color: 'FFFFFF', width: 0 } // ⭐ 无边框
});
// ✅ 正确方案2:使用 line: 'none'(推荐)
slide.addShape('rect', {
x: 1, y: 2, w: 3, h: 4,
fill: { color: 'FFFFFF', transparency: 90 },
line: 'none' // ⭐ 最简洁的无边框写法
});
// ✅ 正确方案3:line 宽度设为 0
slide.addShape('rect', {
x: 1, y: 2, w: 3, h: 4,
fill: { color: 'FFFFFF', transparency: 90 },
line: { width: 0 }
});
```
**⭐ 分析模板边框的方法:**
```python
# 检查形状是否有边框
import re
with open('slide.xml', 'r') as f:
content = f.read()
# 查找所有 定义
lns = re.findall(r']*>(.*?)', content, re.DOTALL)
for ln in lns:
if '' in ln:
print('无边框')
elif '' in ln:
print('有边框,颜色:', re.findall(r'val="([^"]+)"', ln))
else:
print('⚠️ 默认黑色边框(需要在生成时设置 line: none)')
```
### 背景渐变处理
**问题现象:** 正文页背景色与模板不一致(颜色、渐变方向、渐变位置不对)。
**⭐⭐⭐ 模板渐变分析方法:**
模板中的渐变定义示例(来自 slide6.xml):
```xml
```
**⭐ 渐变参数解析:**
```
1. 渐变颜色停止点 ():
- pos="0" -> 0% 位置
- pos="50000" -> 50% 位置
- pos="100000" -> 100% 位置
2. 渐变角度 ():
- ang 值 ÷ 60000 = 实际角度(度)
- ang="0" -> 0° (从左到右)
- ang="5400000" -> 90° (从上到下)
- ang="10800000" -> 180° (从右到左)
- ang="11400000" -> 190° (略微从右上到左下)
3. 透明度 ():
- val="0" -> 完全透明
- val="50000" -> 50% 透明
- val="100000" -> 完全不透明
```
**⭐⭐⭐ pptxgenjs 实现渐变:**
```javascript
// 方案1:使用渐变填充(推荐)
slide.addShape('rect', {
x: 0, y: 0, w: '100%', h: '100%',
fill: {
type: 'gradient',
gradientType: 'linear',
// ⭐ 角度:pptxgenjs 使用 0-360 度
rotate: 190, // 从模板 ang="11400000" 计算: 11400000/60000 = 190°
stops: [
// ⭐ position 是 0-100 的百分比
{ position: 0, color: '0052D9', transparency: 100 }, // 起点透明
{ position: 62, color: '0052D9' }, // 62% 位置不透明
{ position: 100, color: '0052D9' } // 终点不透明
]
},
line: 'none'
});
// 方案2:使用背景图片(如果渐变太复杂)
slide.background = { path: 'backgrounds/gradient-bg.png' };
// 方案3:多层叠加模拟渐变(兼容性更好)
// 底层纯色
slide.background = { color: '0052D9' };
// 上层半透明渐变遮罩
slide.addShape('rect', {
x: 0, y: 0, w: '100%', h: '100%',
fill: {
type: 'gradient',
gradientType: 'linear',
rotate: 190,
stops: [
{ position: 0, color: 'FFFFFF', transparency: 100 },
{ position: 100, color: 'FFFFFF', transparency: 0 }
]
},
line: 'none'
});
```
**⭐ 从模板提取渐变参数的脚本:**
```python
import re
def extract_gradient(xml_content):
"""从 XML 提取渐变参数"""
result = {
'type': None,
'angle': None,
'stops': []
}
# 检查是否有渐变
gradFill = re.search(r']*>(.*?)', xml_content, re.DOTALL)
if not gradFill:
return None
grad_content = gradFill.group(1)
# 提取角度
lin = re.search(r'(.*?)', grad_content, re.DOTALL):
position = int(gs.group(1)) / 1000 # 转换为百分比
gs_content = gs.group(2)
stop = {'position': position}
# 提取颜色
color = re.search(r'srgbClr val="([^"]+)"', gs_content)
if color:
stop['color'] = color.group(1)
# 提取透明度
alpha = re.search(r'
```
**⭐ pptxgenjs 透明度计算:**
```javascript
// XML alpha 值转换为 pptxgenjs transparency
// pptxgenjs: transparency = 100 - (alpha值/1000)
// 示例:alpha val="9876" -> 约10%不透明 -> transparency=90
slide.addShape('rect', {
x: 1, y: 2, w: 1.3, h: 1.3,
fill: {
color: 'FFFFFF',
transparency: 90 // ⭐ 90%透明 = 10%可见
},
line: 'none' // ⭐ 必须!否则有黑边
});
// 示例:alpha val="49687" -> 约50%不透明 -> transparency=50
slide.addShape('rect', {
x: 2, y: 4, w: 0.6, h: 0.6,
fill: {
color: 'FFFFFF',
transparency: 50 // ⭐ 50%透明
},
line: 'none'
});
```
### 配置模板示例(包含边框和渐变)
```javascript
const TEMPLATE_CONFIG = {
// ... 其他配置 ...
// ⭐⭐⭐ 渐变背景配置(从模板 gradFill 提取)
gradients: {
// 正文页渐变(从 slide6.xml 提取)
content: {
type: 'linear',
angle: 190, // ang="11400000" / 60000
stops: [
{ position: 0, color: '0052D9', transparency: 100 }, // 起点透明
{ position: 62, color: '0052D9', transparency: 0 }, // 62%位置不透明
{ position: 100, color: '0052D9', transparency: 0 } // 终点不透明
]
}
},
// ⭐⭐⭐ 装饰形状配置(从模板 sp 元素提取)
decorations: {
// 章节页装饰方块
chapter: [
{
type: 'rect',
x: 1.22, y: 2.31, w: 1.31, h: 1.31,
fill: { color: 'FFFFFF', transparency: 90 }, // alpha=9876
line: 'none' // ⭐ 无边框!
},
{
type: 'rect',
x: 1.95, y: 4.59, w: 0.58, h: 0.58,
fill: { color: 'FFFFFF', transparency: 50 }, // alpha=49687
line: 'none'
}
],
// 目录页装饰方块
toc: [
// ... 从模板提取
]
}
};
```
---
## 依赖
- Node.js 14+
- pptxgenjs 3.x
- Python 3.x (用于文件处理)
```bash
npm install pptxgenjs
```