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,
|
||||
|
||||
Reference in New Issue
Block a user