# html2pptx 通用转化架构
## 1. 目标 Schema 定义
### 顶层结构
```json
{
"presentation": {
"layout": "CUSTOM",
"slideWidth": 24.18, // 英寸
"slideHeight": 17.08 // 英寸
},
"slides": [
{
"background": { "color": "1A1A1A" }, // 可选
"objects": [
// text / shape / image / table / chart
]
}
]
}
```
### text 对象
```json
{
"type": "text",
"text": "文字内容",
"options": {
"x": 1.463, // 英寸(左上角 x)
"y": 1.039, // 英寸(左上角 y)
"w": 2.282, // 英寸(宽度)
"h": 0.25, // 英寸(高度)
"fontSize": 14, // pt
"fontFace": "Arial", // 字体名
"color": "000000", // 6位hex
"bold": false, // 可选
"italic": false, // 可选
"align": "left", // left/center/right/justify
"underline": false, // 可选
"charSpacing": 0 // 可选,字符间距
}
}
```
### shape 对象
```json
{
"type": "shape",
"shapeName": "rect", // rect/roundRect/ellipse
"options": {
"x": 0.83,
"y": 3.07,
"w": 17.78,
"h": 3.12,
"fill": { "color": "ffffff" },
"line": { "type": "none" }, // 可选
"shadow": { // 可选
"type": "outer",
"blur": 15,
"offset": 5,
"color": "999999",
"opacity": 0.5
},
"rectRadius": 0.1, // 可选,英寸
"opacity": 1.0 // 可选
}
}
```
### image 对象
```json
{
"type": "image",
"options": {
"x": 0,
"y": 0,
"w": 10,
"h": 5,
"data": "base64..." // 或 "path": "file.png"
}
}
```
## 2. 通用转化逻辑
### 核心原则
- **不靠命名,靠结构**:slide 容器 = 包含内容子节点的 FRAME
- **不靠猜测,靠数据**:所有属性从 JSON 字段直接读取
- **不硬编码规则**:通过配置或自动推断
### 2.1 自动识别 Slide 容器
```
输入:web-to-ppt JSON 的 nodes 树
输出:slide 容器列表
策略:
1. 遍历 nodes 树,找 FRAME 类型节点
2. 判断条件(满足任一):
a) name 匹配已知模式:slide-N / page / slide+空格 / slide+横杠
b) 结构特征:
- children 数量 > 0
- 面积接近 canvas 面积(> 50% canvas 面积)
- 不是 BODY / HTML / HEAD 等结构性节点
3. 排除条件:
- layerGroup === 'comparison'(截图底图)
- 没有 children
- 面积太小(< 100px × 100px)
```
### 2.2 收集子节点(递归)
```
collectSlideChildren(node, slideX, slideY, parentW, depth):
对于每个子节点:
- 计算相对坐标:rect.x - slideX, rect.y - slideY
- 记录 parentW(父容器宽度)
- 记录 depth(层级深度)
- 跳过 RECTANGLE 类型(截图底图)
- 跳过 layerGroup === 'comparison'
- 递归处理 children
```
### 2.3 收集游离节点
```
collectOrphans(root, slideIds):
slideIds = 所有 slide 容器的 id 集合
遍历 root 树:
- 如果节点 id 在 slideIds 中 → 跳过整个子树
- 否则 → 收集该节点,递归 children
```
### 2.4 映射 TEXT 节点
```
输入:TEXT 类型节点
输出:text 对象
映射规则:
fontSize = parseFloat(styles.fontSize) || 14
fontFace = styles.fontFamily.split(',')[0].trim()
color = rgbaToHex(styles.color) || '000000'
bold = parseInt(styles.fontWeight) >= 700
italic = styles.fontStyle === 'italic'
align = styles.textAlign(非 'start' 时)
underline = styles.textDecorationLine 包含 'underline'
charSpacing = styles.letterSpacing 转换
宽度计算:
contentW = slide 内容宽度(从子节点 x 范围推算)
textW = parentW || rect.w
if (depth === 1) textW = contentW
if (textW > contentW) textW = contentW
if (textW < 108) textW = 108 // 最小 1.5in
if (x + textW > contentW) textW = contentW - x
```
### 2.5 映射 FRAME 节点 → shape
```
输入:FRAME 类型节点(有背景色)
输出:shape 对象
映射规则:
fillColor = rgbaToHex(styles.backgroundColor)
渐变背景 = backgroundImage 提取起始色
圆角 = borderTopLeftRadius 转换
边框 = 四边都有 border 时加 line
阴影 = boxShadow 解析
单边 border = 独立线条 shape
默认 line = { type: 'none' }(去掉 pptxgenjs 默认边框)
```
### 2.6 背景色推断
```
优先级:
1. slide 容器的 styles.backgroundColor(非透明时)
2. canvas.backgroundColor(全局兜底)
3. 从 slide name 推断(dark → #1A1A1A, light → #FAFAFA)
4. 从文字颜色推断(浅色文字 → 深色背景,深色文字 → 浅色背景)
```
## 3. 当前硬编码 → 通用化改造清单
| 硬编码 | 当前逻辑 | 通用化方案 |
|--------|---------|-----------|
| slide 匹配 | `slide-N` / `page` / `slide ` | 结构识别 + 已知模式列表 |
| 游离节点跳过 | `startsWith('slide-')` / `startsWith('slide ')` | 用 slide id 集合判断 |
| 背景色 | `dark`/`light` 关键词 | 优先取 styles,兜底取 canvas |
| TEXT 宽度 | `depth === 1` 判断顶层 | 用 parentW 与 contentW 的关系判断 |
| 边框 | 只处理 borderTop | 四边独立处理 |
## 4. 下一步
1. 重构 `findSlides()` — 支持结构识别
2. 重构 `collectOrphans()` — 用 id 集合判断
3. 重构背景色逻辑 — 多级兜底
4. 补全 styles 映射 — opacity / lineHeight / letterSpacing / textDecoration
5. 测试多个 HTML 样本验证通用性