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:
+67
-15
@@ -279,7 +279,8 @@ function convert(input) {
|
||||
const ctx = { canvasWidth, canvasHeight, assets: input.assets || {} };
|
||||
|
||||
// ===== 直接从 DOM 树中提取每个 slide 的子节点(保留父子关系) =====
|
||||
function collectSlideChildren(node, slideX, slideY, parentW) {
|
||||
function collectSlideChildren(node, slideX, slideY, parentW, depth) {
|
||||
depth = depth || 0;
|
||||
if (!node || !node.rect) return [];
|
||||
const results = [];
|
||||
const nr = node.rect;
|
||||
@@ -292,7 +293,8 @@ function convert(input) {
|
||||
rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
|
||||
styles: node.styles || {}, src: node.src || node.text || '',
|
||||
layerGroup: node.layerGroup || '',
|
||||
parentW: parentW || rw
|
||||
parentW: parentW || rw,
|
||||
_depth: depth
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,13 +306,14 @@ function convert(input) {
|
||||
styles: node.styles || {}, src: '',
|
||||
layerGroup: node.layerGroup || '',
|
||||
parentW: parentW || rw,
|
||||
backgroundImages: node.backgroundImages
|
||||
backgroundImages: node.backgroundImages,
|
||||
_depth: depth
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...collectSlideChildren(child, slideX, slideY, rw));
|
||||
results.push(...collectSlideChildren(child, slideX, slideY, rw, depth + 1));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
@@ -320,7 +323,7 @@ function convert(input) {
|
||||
function findSlides(node) {
|
||||
if (!node || !node.name) return [];
|
||||
const results = [];
|
||||
if (/^slide-\d+$/.test(node.name) || node.name === 'page') {
|
||||
if (/^slide-?\d*$/.test(node.name) || node.name === 'page' || node.name.startsWith('slide ')) {
|
||||
results.push(node);
|
||||
}
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
@@ -358,7 +361,7 @@ function convert(input) {
|
||||
const rw = nr.width ?? nr.w;
|
||||
const rh = nr.height ?? nr.h;
|
||||
// 跳过 slide 容器本身和 raster
|
||||
if (!node.name?.startsWith('slide-') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||
if (!node.name?.startsWith('slide-') && !node.name?.startsWith('slide ') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||
results.push({
|
||||
id: node.id, type: node.type, name: node.name, tag: node.tag,
|
||||
rect: { x: nr.x, y: nr.y, w: rw, h: rh },
|
||||
@@ -368,7 +371,7 @@ function convert(input) {
|
||||
}
|
||||
if (node.children) for (const c of node.children) {
|
||||
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了)
|
||||
if (c.name?.startsWith('slide-') || c.name === 'page') continue;
|
||||
if (c.name?.startsWith('slide-') || c.name?.startsWith('slide ') || c.name === 'page') continue;
|
||||
results.push(...collectOrphans(c));
|
||||
}
|
||||
return results;
|
||||
@@ -415,6 +418,15 @@ function convert(input) {
|
||||
if (hex) slideBg = { color: hex };
|
||||
}
|
||||
}
|
||||
// fallback:从 slide name 推断背景(CSS class 设置的背景 web-to-pixso 没提取)
|
||||
if (!slideBg) {
|
||||
const name = sg.name.toLowerCase();
|
||||
if (name.includes('dark')) {
|
||||
slideBg = { color: '1A1A1A' };
|
||||
} else if (name.includes('light')) {
|
||||
slideBg = { color: 'FAFAFA' };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理 ' + sg.name + ': ' + sg.children.length + ' 个节点');
|
||||
|
||||
@@ -425,6 +437,26 @@ function convert(input) {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 计算 slide 实际内容宽度(用非游离节点推算 padding)
|
||||
var slideW = sg.slideRect.w;
|
||||
var contentW = slideW;
|
||||
// 只用非游离节点(parentW 存在 = 来自 collectSlideChildren,非 orphans)
|
||||
var nonOrphans = sg.children.filter(c => c.parentW != null);
|
||||
if (nonOrphans.length > 0) {
|
||||
var minX = Infinity, maxRight = 0;
|
||||
for (var i = 0; i < nonOrphans.length; i++) {
|
||||
var ch = nonOrphans[i];
|
||||
if (ch.rect) {
|
||||
if (ch.rect.x < minX) minX = ch.rect.x;
|
||||
var right = ch.rect.x + (ch.rect.w || 0);
|
||||
if (right > maxRight) maxRight = right;
|
||||
}
|
||||
}
|
||||
if (minX < Infinity && maxRight > 0) {
|
||||
contentW = maxRight - minX;
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of sg.children) {
|
||||
// 跳过 raster 底图(保留 IMAGE 类型)
|
||||
if (n.type === 'RECTANGLE') { continue; }
|
||||
@@ -452,14 +484,25 @@ function convert(input) {
|
||||
if (st.fontStyle === 'italic') opts.italic = true;
|
||||
if (st.textAlign && st.textAlign !== 'start') opts.align = st.textAlign;
|
||||
|
||||
// 宽度优先用父容器宽度,确保不换行
|
||||
opts.w = Math.round((n.parentW || r.w) / 72 * 1000) / 1000;
|
||||
// 宽度:顶层元素用 contentW,嵌套元素用 parentW,加最小宽度保障
|
||||
var textW = n.parentW || r.w;
|
||||
if (n._depth === 1) {
|
||||
textW = contentW; // 顶层文字用 slide 内容宽度
|
||||
}
|
||||
if (textW > contentW) textW = contentW;
|
||||
if (textW < 108) textW = 108; // 最小 1.5in = 108px
|
||||
// 确保 x + w 不超出 slide 内容区右边界
|
||||
var maxW = contentW - r.x;
|
||||
if (maxW < 108) maxW = 108;
|
||||
if (textW > maxW) textW = maxW;
|
||||
opts.w = Math.round(textW / 72 * 1000) / 1000;
|
||||
|
||||
objects.push({
|
||||
type: 'text',
|
||||
text: n.src,
|
||||
options: opts
|
||||
});
|
||||
continue; // TEXT 节点不参与后续的填充色/边框逻辑
|
||||
}
|
||||
|
||||
// ===== 图片节点 =====
|
||||
@@ -503,18 +546,22 @@ function convert(input) {
|
||||
}
|
||||
}
|
||||
|
||||
// 边框
|
||||
// 边框:只在四边都有 border 时加 shape 的 line,单边由后续"四周边框线"逻辑处理
|
||||
var bw = parseFloat(n.styles.borderTopWidth);
|
||||
if (bw > 0 && n.styles.borderTopStyle !== 'none') {
|
||||
var bbw = parseFloat(n.styles.borderBottomWidth);
|
||||
var blw = parseFloat(n.styles.borderLeftWidth);
|
||||
var brw = parseFloat(n.styles.borderRightWidth);
|
||||
if (bw > 0 && bbw > 0 && blw > 0 && brw > 0 && n.styles.borderTopStyle !== 'none') {
|
||||
var bc = rgbaToHex(n.styles.borderTopColor);
|
||||
if (bc) shapeOpts.line = { color: bc, width: bw };
|
||||
}
|
||||
|
||||
// 阴影
|
||||
if (n.styles.boxShadow && n.styles.boxShadow !== 'none') {
|
||||
var shadowMatch = n.styles.boxShadow.match(/rgba?\([^)]+\)\s+(\d+)px\s+(\d+)px\s+(\d+)px/);
|
||||
var shadowMatch = n.styles.boxShadow.match(/rgba?\(([^)]+)\)\s+(\d+)px\s+(\d+)px\s+(\d+)px/);
|
||||
if (shadowMatch) {
|
||||
shapeOpts.shadow = { type: 'outer', blur: parseInt(shadowMatch[3]), offset: parseInt(shadowMatch[1]), color: '000000', opacity: 0.3 };
|
||||
var shadowColor = rgbaToHex('rgb(' + shadowMatch[1] + ')') || '999999';
|
||||
shapeOpts.shadow = { type: 'outer', blur: parseInt(shadowMatch[4]), offset: parseInt(shadowMatch[3]), color: shadowColor, opacity: 0.5 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,10 +724,15 @@ if (require.main === module) {
|
||||
});
|
||||
} else if (obj.type === 'shape') {
|
||||
const st = { rect: 'rect', roundRect: 'roundRect', ellipse: 'ellipse' }[obj.shapeName] || 'rect';
|
||||
slide.addShape(pres.ShapeType[st] || pres.ShapeType.rect, {
|
||||
const shapeOpts = {
|
||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||
fill: o.fill ? { color: o.fill.color } : undefined
|
||||
});
|
||||
};
|
||||
if (o.line) shapeOpts.line = o.line;
|
||||
else shapeOpts.line = { type: 'none' }; // 去掉 pptxgenjs 默认边框
|
||||
if (o.shadow) shapeOpts.shadow = o.shadow;
|
||||
if (o.rectRadius) shapeOpts.rectRadius = o.rectRadius;
|
||||
slide.addShape(pres.ShapeType[st] || pres.ShapeType.rect, shapeOpts);
|
||||
} else if (obj.type === 'image') {
|
||||
slide.addImage({
|
||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||
|
||||
@@ -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 样本验证通用性
|
||||
@@ -0,0 +1,152 @@
|
||||
# web-to-pixso → PPT 映射分析
|
||||
|
||||
## 1. JSON 结构总览
|
||||
|
||||
### 顶层字段
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `canvas` | 画布尺寸 `{width, height, x, y, backgroundColor}` |
|
||||
| `nodes` | 根节点树(type=FRAME, name=html) |
|
||||
| `assets` | 图片资源 `{key: base64}` |
|
||||
| `fonts` | 字体列表 |
|
||||
| `schemaVersion` | JSON 版本号 |
|
||||
|
||||
### 节点类型
|
||||
| 类型 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| FRAME | 471 | HTML 容器(div, section, table, header 等) |
|
||||
| TEXT | 393 | 文字节点 |
|
||||
| RECTANGLE | 10 | 截图底图(comparison layer,用于视觉对比) |
|
||||
|
||||
### 节点属性
|
||||
```
|
||||
type - FRAME/TEXT/RECTANGLE
|
||||
rect - {x, y, width, height} 像素坐标
|
||||
styles - 57个CSS属性(见下表)
|
||||
name - HTML元素名或文字内容
|
||||
tag - HTML标签名(HTML/DIV/TABLE/TR/TD/SPAN/#text等)
|
||||
text / src - 文字内容(TEXT节点用text,收集时映射为src)
|
||||
layerGroup - editable/text/comparison
|
||||
backgroundImages - 图片资源引用数组
|
||||
clipRect - 裁剪区域
|
||||
visibleRect - 可见区域
|
||||
clipped - 是否被裁剪
|
||||
id - 节点ID
|
||||
section - document
|
||||
visualRole - layout-wrapper/text-node/raster-fallback
|
||||
```
|
||||
|
||||
## 2. 57个 styles 字段分类
|
||||
|
||||
### 已处理(15个)
|
||||
| 字段 | 用途 | 当前处理方式 |
|
||||
|------|------|-------------|
|
||||
| backgroundColor | 背景色 | → shape fill |
|
||||
| backgroundImage | 渐变背景 | → 提取起始色做纯色 fill |
|
||||
| borderTopWidth/Color/Style | 顶部边框 | → shape line(四边都有时)或单独线条 |
|
||||
| borderBottomWidth/Color/Style | 底部边框 | 同上 |
|
||||
| borderLeftWidth/Color/Style | 左边框 | 同上 |
|
||||
| borderRightWidth/Color/Style | 右边框 | 同上 |
|
||||
| borderTopLeftRadius | 圆角 | → rectRadius |
|
||||
| boxShadow | 阴影 | → shadow |
|
||||
| fontSize | 字号 | → fontSize(pt) |
|
||||
| fontFamily | 字体 | → fontFace |
|
||||
| color | 文字颜色 | → color |
|
||||
| fontWeight | 粗体 | → bold |
|
||||
| fontStyle | 斜体 | → italic |
|
||||
| textAlign | 对齐 | → align |
|
||||
|
||||
### 未处理(42个)
|
||||
| 字段 | 重要性 | 说明 |
|
||||
|------|--------|------|
|
||||
| **opacity** | 高 | 元素透明度,PPT支持 `opacity` |
|
||||
| **lineHeight** | 高 | 行高,影响文字换行和间距 |
|
||||
| **letterSpacing** | 中 | 字间距,PPT支持 `charSpacing` |
|
||||
| **textDecorationLine** | 中 | 下划线/删除线 |
|
||||
| **textTransform** | 低 | 大小写转换 |
|
||||
| **whiteSpace** | 高 | 控制换行行为(nowrap等) |
|
||||
| **wordBreak** | 中 | 断词规则 |
|
||||
| **visibility** | 中 | visible/hidden |
|
||||
| **display** | 中 | none时不渲染 |
|
||||
| **transform** | 中 | 旋转/缩放/位移 |
|
||||
| **transformOrigin** | 低 | 变换原点 |
|
||||
| **zIndex** | 高 | 层级顺序 |
|
||||
| **overflow** | 中 | 溢出处理 |
|
||||
| **clipPath** | 低 | 裁剪路径 |
|
||||
| **filter** | 低 | CSS滤镜 |
|
||||
| **borderTopRightRadius** | 中 | 右上圆角(目前只用左上) |
|
||||
| **borderBottomLeftRadius** | 中 | 左下圆角 |
|
||||
| **borderBottomRightRadius** | 中 | 右下圆角 |
|
||||
| **backgroundPosition** | 低 | 背景定位 |
|
||||
| **backgroundSize** | 低 | 背景尺寸 |
|
||||
| **backgroundRepeat** | 低 | 背景重复 |
|
||||
| **backgroundClip** | 低 | 背景裁剪 |
|
||||
| **backgroundOrigin** | 低 | 背景原点 |
|
||||
| **position** | 低 | 定位方式 |
|
||||
| **objectFit** | 低 | 图片适配 |
|
||||
| **objectPosition** | 低 | 图片定位 |
|
||||
| **maskImage/MaskPosition/MaskSize** | 低 | 遮罩 |
|
||||
| **webkitMaskImage/MaskPosition/MaskSize** | 低 | 遮罩(webkit) |
|
||||
| **backdropFilter** | 低 | 背景滤镜 |
|
||||
|
||||
## 3. 映射优先级
|
||||
|
||||
### P0(必须处理)
|
||||
1. **opacity** → shape/text 的 opacity 属性
|
||||
2. **whiteSpace** → 控制文字是否换行
|
||||
3. **zIndex** → 调整 objects 数组顺序(后渲染在上层)
|
||||
4. **lineHeight** → 文字行高(paraSpaceBefore/after 或 lineSpacingMultiple)
|
||||
|
||||
### P1(应该处理)
|
||||
5. **letterSpacing** → charSpacing(字符间距)
|
||||
6. **textDecorationLine** → underline/strike
|
||||
7. **display:none** → 跳过该节点
|
||||
8. **visibility:hidden** → 跳过该节点
|
||||
9. **四个圆角独立处理** → 当前只用了 borderTopLeftRadius
|
||||
|
||||
### P2(可以处理)
|
||||
10. **transform** → rotate 属性
|
||||
11. **wordBreak** → 辅助判断换行
|
||||
12. **overflow** → 辅助判断裁剪
|
||||
13. **filter** → 特殊效果(大部分PPT不支持)
|
||||
|
||||
## 4. 当前代码架构
|
||||
|
||||
```
|
||||
HTML → web-to-pixso 插件 → JSON
|
||||
↓
|
||||
convert-w2p.js(提取+映射)
|
||||
↓
|
||||
Schema JSON
|
||||
↓
|
||||
pptxgenjs(渲染)
|
||||
↓
|
||||
.pptx 文件
|
||||
```
|
||||
|
||||
### convert-w2p.js 处理流程
|
||||
1. findSlides() — 找 page/slide-N 容器
|
||||
2. collectSlideChildren() — 递归提取子节点(保留层级)
|
||||
3. collectOrphans() — 收集游离节点归入最近 slide
|
||||
4. 排序:FRAME 在前,TEXT 在后
|
||||
5. 映射循环:遍历每个子节点
|
||||
- TEXT → text 对象(含字号/字体/颜色/粗体/对齐)
|
||||
- IMAGE → image 对象
|
||||
- FRAME with bg → shape 对象(含 fill/border/shadow/rectRadius)
|
||||
- 四边 border → 独立线条 shape
|
||||
6. 输出 Schema JSON + 调用 pptxgenjs 生成 PPTX
|
||||
|
||||
## 5. 下一步行动
|
||||
|
||||
### 短期(本次迭代)
|
||||
- [ ] 实现 P0:opacity、whiteSpace、zIndex、lineHeight
|
||||
- [ ] 实现 P1:letterSpacing、textDecoration、display/visibility 过滤
|
||||
|
||||
### 中期
|
||||
- [ ] 建立 web-to-pixso JSON 的完整字段文档
|
||||
- [ ] 验证不同 HTML 结构的 JSON 输出一致性
|
||||
- [ ] 处理 transform(旋转)
|
||||
|
||||
### 长期
|
||||
- [ ] 参考设计稿,实现高度还原的 PPT 输出
|
||||
- [ ] 处理复杂布局(grid/flex 嵌套)
|
||||
Reference in New Issue
Block a user