通用 PPT 生成技能
概述
此技能可以根据任意用户指定的 PPT 模板,结合提供的图文素材(文档、图片等),自动生成一份风格统一的演示文稿。
⭐⭐⭐ 核心理念:每个模板都是独特的,必须针对性分析!
不同 PPT 模板使用的字体、对齐方式、字号、颜色、位置、背景样式都完全不同。绝不能将一个模板的配置直接应用到另一个模板!每次使用新模板时,都必须重新分析 XML 提取精确参数。
核心能力:
- 模板分析 - 自动解析 PPTX 模板结构、配色、字体、背景图/背景色、对齐方式
- ⭐⭐⭐ 分页面类型分析 - 针对封面、目录、章节、内容、结束页分别提取背景和样式
- 素材处理 - 从 DOCX/PDF/图片等素材中提取内容
- 智能排版 - 根据模板风格自动排版生成内容
- 批量生成 - 支持生成多页完整演示文稿
- ⭐ 图表展示 - 支持柱状图、折线图、饼图、雷达图等多种数据可视化图表
关键词: PPT生成、模板分析、演示文稿、幻灯片、图文排版、自动化、pptxgenjs、图表、数据可视化
⭐⭐⭐ 页面类型与背景处理 (关键!)
五种核心页面类型
不同类型的页面可能使用不同的背景处理方式:
| 页面类型 | 典型特征 | 背景处理方式 |
|---|---|---|
| 封面页 (Cover) | 主标题 + 副标题 + Logo | 背景图/渐变/斜切形状 |
| 目录页 (TOC) | 目录列表 + 装饰元素 | 纯色背景 + 装饰形状 |
| 章节页 (Chapter) | 大号章节编号 + 章节标题 | 纯色背景 + 装饰形状 |
| 内容页 (Content) | 标题 + 正文/图片/图表 | 纯色背景/背景图 |
| 结束页 (Thanks) | 感谢语 + 联系方式 | 纯色背景 + 装饰形状 |
⭐⭐⭐ 背景类型分析
PPT 背景有三种主要类型:
1. 纯色背景 (SolidFill)
<!-- 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 对应代码:
slide.background = { color: '0D1E43' }; // 深蓝色
2. 背景图片 (BlipFill)
<!-- 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 对应代码:
slide.background = { path: 'workspace/backgrounds/cover-bg.png' };
3. 渐变背景 (GradFill)
<!-- 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 对应代码:
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 提取主题色映射:
cat workspace/template-analysis/ppt/theme/theme1.xml | grep -E "dk1|lt1|dk2|lt2|accent" | head -20
⭐⭐⭐ 分页面类型完整分析流程
# 完整的页面背景分析脚本
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 中提取:
- 精确字号 -
sz="8000"表示 80pt (sz ÷ 100) - 精确位置 -
<a:off x="..." y="..."/>EMU单位,转换为英寸或百分比 - 精确颜色 -
srgbClr val="FFFFFF"直接使用 - 字体粗细 -
b="1"表示粗体 - ⭐ 对齐方式 -
algn="ctr"居中,algn="r"右对齐,algn="l"或无属性表示左对齐
1.1 解压 PPTX 模板
PPTX 文件本质是 ZIP 压缩包,包含 XML 和媒体资源:
# 创建工作目录
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 脚本精确提取参数(包含字体、边框、渐变信息):
# 分析幻灯片的精确参数(含字体、边框、渐变)
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 提取配色方案:
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 结构 - 颜色方案:
<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 识别幻灯片类型与背景图映射
查看布局关系:
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
查看布局对应的背景图:
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 提取背景图
# 根据分析结果复制背景图
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 文档
# 使用 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 提取文本内容
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 内容结构化
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 模板会使用不同的字体、对齐方式、字号、颜色等,必须针对每个模板单独分析!
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 默认会根据文本框宽度自动换行。解决方案:
- 使用
fit: 'shrink'- 自动缩小字号以适应宽度(推荐) - 使用足够大的宽度 - 确保文本框宽度足够容纳所有文字
- 根据文字长度动态计算宽度 - 中文约每字符 0.7-1.0 × 字号(pt) / 72 英寸
// ⭐ 计算文本所需宽度的辅助函数
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 中按相同顺序重建
⭐⭐⭐ 通用背景处理函数:
根据模板分析结果,不同页面可能使用不同的背景类型。以下是通用的背景处理方案:
// ⭐⭐⭐ 通用背景设置函数
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 不直接支持自定义路径斜切,可使用以下方案模拟:
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 属性精确提取!
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 章节页生成函数
⚠️ 注意:章节标题和副标题必须使用模板的对齐方式(通常是居中)!
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 内容页生成函数
⭐⭐⭐ 关键:正文页背景可能是渐变,需要从模板精确提取!
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 数据卡片页生成函数
⭐ 注意:卡片背景形状必须正确处理边框!
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 |
散点图 |
图表数据结构
// ⭐ 图表数据的标准格式
const chart
…(truncated)