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:
李进
2026-07-24 16:30:24 +08:00
parent 56daab6b67
commit d079a4b4cc
3 changed files with 422 additions and 15 deletions
+67 -15
View File
@@ -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,