- 文本框宽度:contentW 从 slide 子节点推算,排除游离节点
- TEXT 宽度:顶层用 contentW,嵌套用 parentW,最小1.5in,x+w≤slideW
- collectSlideChildren 新增 _depth 参数标记层级
- boxShadow 正确传给 pptxgenjs(之前 addShape 只传了 fill)
- 单边 border 不加 shape line,由四周边框线逻辑单独处理
- shape 默认 line: { type: 'none' } 去掉 pptxgenjs 默认边框
- 背景色:从 slide name 推断 dark/light 兜底
- findSlides 支持 'slide ' 前缀
- collectOrphans 跳过 'slide ' 子树(修复文字重复)
- 新增 docs/ 目录:映射分析和通用架构文档
204 lines
5.5 KiB
Markdown
204 lines
5.5 KiB
Markdown
# 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-pixso 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 样本验证通用性
|