- 文本框宽度: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/ 目录:映射分析和通用架构文档
5.5 KiB
5.5 KiB
html2pptx 通用转化架构
1. 目标 Schema 定义
顶层结构
{
"presentation": {
"layout": "CUSTOM",
"slideWidth": 24.18, // 英寸
"slideHeight": 17.08 // 英寸
},
"slides": [
{
"background": { "color": "1A1A1A" }, // 可选
"objects": [
// text / shape / image / table / chart
]
}
]
}
text 对象
{
"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 对象
{
"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 对象
{
"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. 下一步
- 重构
findSlides()— 支持结构识别 - 重构
collectOrphans()— 用 id 集合判断 - 重构背景色逻辑 — 多级兜底
- 补全 styles 映射 — opacity / lineHeight / letterSpacing / textDecoration
- 测试多个 HTML 样本验证通用性