fix: 文本框宽度/阴影/边框/背景/去重 — 通用化前稳定版
- 文本框宽度: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/ 目录:映射分析和通用架构文档
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# 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 样本验证通用性
|
||||
Reference in New Issue
Block a user