Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa28f1bd8 | ||
|
|
dd916dcf9e | ||
|
|
0e60f7525b | ||
|
|
a2c7f23107 |
+117
-30
@@ -318,23 +318,58 @@ function convert(input) {
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 找到所有 slide 容器(name 为 slide-N 或 page 的节点)
|
||||
function findSlides(node) {
|
||||
if (!node || !node.name) return [];
|
||||
// ===== 通用 slide 识别 =====
|
||||
// 收集 slide 容器子树的所有 id(用于游离节点判断)
|
||||
function collectIds(node, set) {
|
||||
if (!node) return;
|
||||
if (node.id) set.add(node.id);
|
||||
if (node.children) for (const c of node.children) collectIds(c, set);
|
||||
}
|
||||
|
||||
// 已知 slide 命名模式
|
||||
const SLIDE_NAME_RE = /^(slide[- ]?|page)/i;
|
||||
// 结构性节点(不应作为 slide 容器)
|
||||
const STRUCTURAL_TAGS = new Set(['HTML', 'BODY', 'HEAD', 'CANVAS']);
|
||||
|
||||
function isSlideCandidate(node, canvasArea) {
|
||||
if (!node || node.type !== 'FRAME') return false;
|
||||
if (!node.rect) return false;
|
||||
// 排除截图底图
|
||||
if (node.layerGroup === 'comparison') return false;
|
||||
// 排除结构性节点
|
||||
if (STRUCTURAL_TAGS.has(node.tag)) return false;
|
||||
// 必须有 children
|
||||
if (!node.children || node.children.length === 0) return false;
|
||||
// 已知命名模式
|
||||
if (node.name && SLIDE_NAME_RE.test(node.name.toLowerCase())) return true;
|
||||
// 结构特征:面积 > 50% canvas
|
||||
const rw = node.rect.width ?? node.rect.w ?? 0;
|
||||
const rh = node.rect.height ?? node.rect.h ?? 0;
|
||||
const area = rw * rh;
|
||||
if (canvasArea > 0 && area > canvasArea * 0.5) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function findSlides(node, canvasArea) {
|
||||
if (!node) return [];
|
||||
const results = [];
|
||||
if (/^slide-?\d*$/.test(node.name) || node.name === 'page' || node.name.startsWith('slide ')) {
|
||||
results.push(node);
|
||||
}
|
||||
// 先递归收集子节点中的 slide candidates
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findSlides(child));
|
||||
results.push(...findSlides(child, canvasArea));
|
||||
}
|
||||
}
|
||||
// 如果子节点中已有 slide candidates,当前节点是容器,不是 slide
|
||||
if (results.length > 0) return results;
|
||||
// 当前节点是 slide candidate
|
||||
if (isSlideCandidate(node, canvasArea)) {
|
||||
results.push(node);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const slideContainers = findSlides(inputRoot);
|
||||
|
||||
const canvasArea = (canvas.width || 0) * (canvas.height || 0);
|
||||
const slideContainers = findSlides(inputRoot, canvasArea);
|
||||
console.log(`找到 ${slideContainers.length} 个 slide 容器`);
|
||||
|
||||
const slideGroups = slideContainers.map(s => ({
|
||||
@@ -352,16 +387,21 @@ function convert(input) {
|
||||
|
||||
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
|
||||
|
||||
// ===== fallback: 收集不在 slide 子树内但坐标在 slide 范围内的节点 =====
|
||||
// 从根节点收集所有非 slide 节点
|
||||
// ===== 通用游离节点收集 =====
|
||||
// 构建 slide 容器子树的 id 集合
|
||||
const slideSubtreeIds = new Set();
|
||||
for (const sc of slideContainers) {
|
||||
collectIds(sc, slideSubtreeIds);
|
||||
}
|
||||
|
||||
function collectOrphans(node) {
|
||||
if (!node || !node.rect) return [];
|
||||
const results = [];
|
||||
const nr = node.rect;
|
||||
const rw = nr.width ?? nr.w;
|
||||
const rh = nr.height ?? nr.h;
|
||||
// 跳过 slide 容器本身和 raster
|
||||
if (!node.name?.startsWith('slide-') && !node.name?.startsWith('slide ') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||
// 用 id 集合判断:不在任何 slide 子树内的节点才是游离节点
|
||||
if (!slideSubtreeIds.has(node.id) && 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 },
|
||||
@@ -370,8 +410,8 @@ function convert(input) {
|
||||
});
|
||||
}
|
||||
if (node.children) for (const c of node.children) {
|
||||
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了)
|
||||
if (c.name?.startsWith('slide-') || c.name?.startsWith('slide ') || c.name === 'page') continue;
|
||||
// 跳过已在 slide 子树内的节点
|
||||
if (slideSubtreeIds.has(c.id)) continue;
|
||||
results.push(...collectOrphans(c));
|
||||
}
|
||||
return results;
|
||||
@@ -418,15 +458,25 @@ 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' };
|
||||
// fallback:从 slide 容器的文字颜色反推背景
|
||||
if (!slideBg && slideContainer && slideContainer.styles) {
|
||||
const textColor = slideContainer.styles.color;
|
||||
if (textColor) {
|
||||
const tc = rgbaToHex(textColor);
|
||||
if (tc) {
|
||||
const r = parseInt(tc.slice(0,2), 16);
|
||||
const g = parseInt(tc.slice(2,4), 16);
|
||||
const b = parseInt(tc.slice(4,6), 16);
|
||||
const lum = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
slideBg = lum > 128 ? { color: '1A1A1A' } : { color: 'FAFAFA' };
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback:从 canvas 背景色兜底(最后手段)
|
||||
if (!slideBg && canvas.backgroundColor && canvas.backgroundColor !== 'rgba(0, 0, 0, 0)') {
|
||||
const hex = rgbaToHex(canvas.backgroundColor);
|
||||
if (hex) slideBg = { color: hex };
|
||||
}
|
||||
|
||||
console.log('处理 ' + sg.name + ': ' + sg.children.length + ' 个节点');
|
||||
|
||||
@@ -474,6 +524,9 @@ function convert(input) {
|
||||
|
||||
// text 节点
|
||||
if (n.type === 'TEXT' && n.src) {
|
||||
// 跳过不可见节点
|
||||
if (n.styles.display === 'none' || n.styles.visibility === 'hidden') { continue; }
|
||||
|
||||
const st = n.styles;
|
||||
const fs = parseFloat(st.fontSize) || 14;
|
||||
var pt = Math.round(fs);
|
||||
@@ -483,12 +536,32 @@ function convert(input) {
|
||||
if (parseInt(st.fontWeight) >= 700) opts.bold = true;
|
||||
if (st.fontStyle === 'italic') opts.italic = true;
|
||||
if (st.textAlign && st.textAlign !== 'start') opts.align = st.textAlign;
|
||||
|
||||
// 宽度:顶层元素用 contentW,嵌套元素用 parentW,加最小宽度保障
|
||||
var textW = n.parentW || r.w;
|
||||
if (n._depth === 1) {
|
||||
textW = contentW; // 顶层文字用 slide 内容宽度
|
||||
// 文字装饰
|
||||
if (st.textDecorationLine) {
|
||||
if (st.textDecorationLine.includes('underline')) opts.underline = true;
|
||||
if (st.textDecorationLine.includes('line-through')) opts.strike = 'sngStrike';
|
||||
}
|
||||
// 字符间距
|
||||
if (st.letterSpacing && st.letterSpacing !== 'normal') {
|
||||
var ls = parseFloat(st.letterSpacing);
|
||||
if (!isNaN(ls) && ls !== 0) opts.charSpacing = Math.round(ls * 100 / fs); // 转为百分比
|
||||
}
|
||||
// 透明度
|
||||
if (st.opacity !== undefined && st.opacity !== '' && parseFloat(st.opacity) < 1) {
|
||||
opts.transparency = Math.round((1 - parseFloat(st.opacity)) * 100);
|
||||
}
|
||||
// 行高
|
||||
if (st.lineHeight && st.lineHeight !== 'normal') {
|
||||
var lh = parseFloat(st.lineHeight);
|
||||
if (!isNaN(lh) && lh > 0) {
|
||||
var lhRatio = lh / fs;
|
||||
if (lhRatio > 0.5 && lhRatio < 5) opts.lineSpacingMultiple = Math.round(lhRatio * 100) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
// 宽度:parentW 超过 contentW 的 80% 时视为顶层,用 contentW;否则用 parentW
|
||||
var textW = n.parentW || r.w;
|
||||
if (textW > contentW * 0.8) textW = contentW;
|
||||
if (textW > contentW) textW = contentW;
|
||||
if (textW < 108) textW = 108; // 最小 1.5in = 108px
|
||||
// 确保 x + w 不超出 slide 内容区右边界
|
||||
@@ -521,6 +594,9 @@ function convert(input) {
|
||||
}
|
||||
|
||||
// ===== 提取填充色 =====
|
||||
// 跳过不可见节点
|
||||
if (n.styles.display === 'none' || n.styles.visibility === 'hidden') { continue; }
|
||||
|
||||
let fillColor = null;
|
||||
const bg = n.styles.backgroundColor;
|
||||
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
||||
@@ -535,6 +611,10 @@ function convert(input) {
|
||||
}
|
||||
if (fillColor) {
|
||||
var shapeOpts = { x: opts.x, y: opts.y, w: opts.w, h: opts.h, fill: { color: fillColor } };
|
||||
// 透明度
|
||||
if (n.styles.opacity !== undefined && n.styles.opacity !== '' && parseFloat(n.styles.opacity) < 1) {
|
||||
shapeOpts.transparency = Math.round((1 - parseFloat(n.styles.opacity)) * 100);
|
||||
}
|
||||
|
||||
// 圆角
|
||||
var brVal = n.styles.borderTopLeftRadius;
|
||||
@@ -714,14 +794,20 @@ if (require.main === module) {
|
||||
for (const obj of (slideData.objects || [])) {
|
||||
const o = obj.options || {};
|
||||
if (obj.type === 'text') {
|
||||
slide.addText(obj.text || '', {
|
||||
const textOpts = {
|
||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||
fontSize: o.fontSize || 12,
|
||||
fontFace: o.fontFace || 'Arial',
|
||||
color: o.color || '000000',
|
||||
bold: o.bold || false,
|
||||
align: o.align || 'left'
|
||||
});
|
||||
};
|
||||
if (o.underline) textOpts.underline = o.underline;
|
||||
if (o.strike) textOpts.strike = o.strike;
|
||||
if (o.charSpacing) textOpts.charSpacing = o.charSpacing;
|
||||
if (o.transparency !== undefined) textOpts.transparency = o.transparency;
|
||||
if (o.lineSpacingMultiple) textOpts.lineSpacingMultiple = o.lineSpacingMultiple;
|
||||
slide.addText(obj.text || '', textOpts);
|
||||
} else if (obj.type === 'shape') {
|
||||
const st = { rect: 'rect', roundRect: 'roundRect', ellipse: 'ellipse' }[obj.shapeName] || 'rect';
|
||||
const shapeOpts = {
|
||||
@@ -732,6 +818,7 @@ if (require.main === module) {
|
||||
else shapeOpts.line = { type: 'none' }; // 去掉 pptxgenjs 默认边框
|
||||
if (o.shadow) shapeOpts.shadow = o.shadow;
|
||||
if (o.rectRadius) shapeOpts.rectRadius = o.rectRadius;
|
||||
if (o.transparency !== undefined) shapeOpts.transparency = o.transparency;
|
||||
slide.addShape(pres.ShapeType[st] || pres.ShapeType.rect, shapeOpts);
|
||||
} else if (obj.type === 'image') {
|
||||
slide.addImage({
|
||||
|
||||
Reference in New Issue
Block a user