Compare commits
4
Commits
d079a4b4cc
...
aaa28f1bd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa28f1bd8 | ||
|
|
dd916dcf9e | ||
|
|
0e60f7525b | ||
|
|
a2c7f23107 |
+116
-29
@@ -318,23 +318,58 @@ function convert(input) {
|
|||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
// ===== 通用 slide 识别 =====
|
||||||
// 找到所有 slide 容器(name 为 slide-N 或 page 的节点)
|
// 收集 slide 容器子树的所有 id(用于游离节点判断)
|
||||||
function findSlides(node) {
|
function collectIds(node, set) {
|
||||||
if (!node || !node.name) return [];
|
if (!node) return;
|
||||||
const results = [];
|
if (node.id) set.add(node.id);
|
||||||
if (/^slide-?\d*$/.test(node.name) || node.name === 'page' || node.name.startsWith('slide ')) {
|
if (node.children) for (const c of node.children) collectIds(c, set);
|
||||||
results.push(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已知 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 = [];
|
||||||
|
// 先递归收集子节点中的 slide candidates
|
||||||
if (node.children && Array.isArray(node.children)) {
|
if (node.children && Array.isArray(node.children)) {
|
||||||
for (const child of 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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
const slideContainers = findSlides(inputRoot);
|
const canvasArea = (canvas.width || 0) * (canvas.height || 0);
|
||||||
|
const slideContainers = findSlides(inputRoot, canvasArea);
|
||||||
console.log(`找到 ${slideContainers.length} 个 slide 容器`);
|
console.log(`找到 ${slideContainers.length} 个 slide 容器`);
|
||||||
|
|
||||||
const slideGroups = slideContainers.map(s => ({
|
const slideGroups = slideContainers.map(s => ({
|
||||||
@@ -352,16 +387,21 @@ function convert(input) {
|
|||||||
|
|
||||||
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
|
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) {
|
function collectOrphans(node) {
|
||||||
if (!node || !node.rect) return [];
|
if (!node || !node.rect) return [];
|
||||||
const results = [];
|
const results = [];
|
||||||
const nr = node.rect;
|
const nr = node.rect;
|
||||||
const rw = nr.width ?? nr.w;
|
const rw = nr.width ?? nr.w;
|
||||||
const rh = nr.height ?? nr.h;
|
const rh = nr.height ?? nr.h;
|
||||||
// 跳过 slide 容器本身和 raster
|
// 用 id 集合判断:不在任何 slide 子树内的节点才是游离节点
|
||||||
if (!node.name?.startsWith('slide-') && !node.name?.startsWith('slide ') && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
if (!slideSubtreeIds.has(node.id) && node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
|
||||||
results.push({
|
results.push({
|
||||||
id: node.id, type: node.type, name: node.name, tag: node.tag,
|
id: node.id, type: node.type, name: node.name, tag: node.tag,
|
||||||
rect: { x: nr.x, y: nr.y, w: rw, h: rh },
|
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) {
|
if (node.children) for (const c of node.children) {
|
||||||
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了)
|
// 跳过已在 slide 子树内的节点
|
||||||
if (c.name?.startsWith('slide-') || c.name?.startsWith('slide ') || c.name === 'page') continue;
|
if (slideSubtreeIds.has(c.id)) continue;
|
||||||
results.push(...collectOrphans(c));
|
results.push(...collectOrphans(c));
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@@ -418,15 +458,25 @@ function convert(input) {
|
|||||||
if (hex) slideBg = { color: hex };
|
if (hex) slideBg = { color: hex };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fallback:从 slide name 推断背景(CSS class 设置的背景 web-to-pixso 没提取)
|
// fallback:从 slide 容器的文字颜色反推背景
|
||||||
if (!slideBg) {
|
if (!slideBg && slideContainer && slideContainer.styles) {
|
||||||
const name = sg.name.toLowerCase();
|
const textColor = slideContainer.styles.color;
|
||||||
if (name.includes('dark')) {
|
if (textColor) {
|
||||||
slideBg = { color: '1A1A1A' };
|
const tc = rgbaToHex(textColor);
|
||||||
} else if (name.includes('light')) {
|
if (tc) {
|
||||||
slideBg = { color: 'FAFAFA' };
|
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 + ' 个节点');
|
console.log('处理 ' + sg.name + ': ' + sg.children.length + ' 个节点');
|
||||||
|
|
||||||
@@ -474,6 +524,9 @@ function convert(input) {
|
|||||||
|
|
||||||
// text 节点
|
// text 节点
|
||||||
if (n.type === 'TEXT' && n.src) {
|
if (n.type === 'TEXT' && n.src) {
|
||||||
|
// 跳过不可见节点
|
||||||
|
if (n.styles.display === 'none' || n.styles.visibility === 'hidden') { continue; }
|
||||||
|
|
||||||
const st = n.styles;
|
const st = n.styles;
|
||||||
const fs = parseFloat(st.fontSize) || 14;
|
const fs = parseFloat(st.fontSize) || 14;
|
||||||
var pt = Math.round(fs);
|
var pt = Math.round(fs);
|
||||||
@@ -483,12 +536,32 @@ function convert(input) {
|
|||||||
if (parseInt(st.fontWeight) >= 700) opts.bold = true;
|
if (parseInt(st.fontWeight) >= 700) opts.bold = true;
|
||||||
if (st.fontStyle === 'italic') opts.italic = true;
|
if (st.fontStyle === 'italic') opts.italic = true;
|
||||||
if (st.textAlign && st.textAlign !== 'start') opts.align = st.textAlign;
|
if (st.textAlign && st.textAlign !== 'start') opts.align = st.textAlign;
|
||||||
|
// 文字装饰
|
||||||
// 宽度:顶层元素用 contentW,嵌套元素用 parentW,加最小宽度保障
|
if (st.textDecorationLine) {
|
||||||
var textW = n.parentW || r.w;
|
if (st.textDecorationLine.includes('underline')) opts.underline = true;
|
||||||
if (n._depth === 1) {
|
if (st.textDecorationLine.includes('line-through')) opts.strike = 'sngStrike';
|
||||||
textW = contentW; // 顶层文字用 slide 内容宽度
|
|
||||||
}
|
}
|
||||||
|
// 字符间距
|
||||||
|
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 > contentW) textW = contentW;
|
||||||
if (textW < 108) textW = 108; // 最小 1.5in = 108px
|
if (textW < 108) textW = 108; // 最小 1.5in = 108px
|
||||||
// 确保 x + w 不超出 slide 内容区右边界
|
// 确保 x + w 不超出 slide 内容区右边界
|
||||||
@@ -521,6 +594,9 @@ function convert(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 提取填充色 =====
|
// ===== 提取填充色 =====
|
||||||
|
// 跳过不可见节点
|
||||||
|
if (n.styles.display === 'none' || n.styles.visibility === 'hidden') { continue; }
|
||||||
|
|
||||||
let fillColor = null;
|
let fillColor = null;
|
||||||
const bg = n.styles.backgroundColor;
|
const bg = n.styles.backgroundColor;
|
||||||
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
||||||
@@ -535,6 +611,10 @@ function convert(input) {
|
|||||||
}
|
}
|
||||||
if (fillColor) {
|
if (fillColor) {
|
||||||
var shapeOpts = { x: opts.x, y: opts.y, w: opts.w, h: opts.h, fill: { color: 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;
|
var brVal = n.styles.borderTopLeftRadius;
|
||||||
@@ -714,14 +794,20 @@ if (require.main === module) {
|
|||||||
for (const obj of (slideData.objects || [])) {
|
for (const obj of (slideData.objects || [])) {
|
||||||
const o = obj.options || {};
|
const o = obj.options || {};
|
||||||
if (obj.type === 'text') {
|
if (obj.type === 'text') {
|
||||||
slide.addText(obj.text || '', {
|
const textOpts = {
|
||||||
x: o.x, y: o.y, w: o.w, h: o.h,
|
x: o.x, y: o.y, w: o.w, h: o.h,
|
||||||
fontSize: o.fontSize || 12,
|
fontSize: o.fontSize || 12,
|
||||||
fontFace: o.fontFace || 'Arial',
|
fontFace: o.fontFace || 'Arial',
|
||||||
color: o.color || '000000',
|
color: o.color || '000000',
|
||||||
bold: o.bold || false,
|
bold: o.bold || false,
|
||||||
align: o.align || 'left'
|
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') {
|
} else if (obj.type === 'shape') {
|
||||||
const st = { rect: 'rect', roundRect: 'roundRect', ellipse: 'ellipse' }[obj.shapeName] || 'rect';
|
const st = { rect: 'rect', roundRect: 'roundRect', ellipse: 'ellipse' }[obj.shapeName] || 'rect';
|
||||||
const shapeOpts = {
|
const shapeOpts = {
|
||||||
@@ -732,6 +818,7 @@ if (require.main === module) {
|
|||||||
else shapeOpts.line = { type: 'none' }; // 去掉 pptxgenjs 默认边框
|
else shapeOpts.line = { type: 'none' }; // 去掉 pptxgenjs 默认边框
|
||||||
if (o.shadow) shapeOpts.shadow = o.shadow;
|
if (o.shadow) shapeOpts.shadow = o.shadow;
|
||||||
if (o.rectRadius) shapeOpts.rectRadius = o.rectRadius;
|
if (o.rectRadius) shapeOpts.rectRadius = o.rectRadius;
|
||||||
|
if (o.transparency !== undefined) shapeOpts.transparency = o.transparency;
|
||||||
slide.addShape(pres.ShapeType[st] || pres.ShapeType.rect, shapeOpts);
|
slide.addShape(pres.ShapeType[st] || pres.ShapeType.rect, shapeOpts);
|
||||||
} else if (obj.type === 'image') {
|
} else if (obj.type === 'image') {
|
||||||
slide.addImage({
|
slide.addImage({
|
||||||
|
|||||||
Reference in New Issue
Block a user