# 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 样本验证通用性