feat: complete web-to-pixso JSON mapping - border sides, image assets, orphan nodes, slide container borders

This commit is contained in:
李进
2026-07-24 13:22:28 +08:00
parent 7735bb75de
commit 8802b00bdc
+146 -15
View File
@@ -279,25 +279,38 @@ function convert(input) {
const ctx = { canvasWidth, canvasHeight, assets: input.assets || {} };
// ===== 直接从 DOM 树中提取每个 slide 的子节点(保留父子关系) =====
function collectSlideChildren(node, slideX, slideY) {
function collectSlideChildren(node, slideX, slideY, parentW) {
if (!node || !node.rect) return [];
const results = [];
const nr = node.rect;
const rw = nr.width ?? nr.w;
const rh = nr.height ?? nr.h;
// 保留所有非 raster 节点
if (node.type !== 'RECTANGLE' && node.layerGroup !== 'comparison') {
results.push({
id: node.id, type: node.type, name: node.name, tag: node.tag,
rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
styles: node.styles || {}, src: node.src || node.text || '',
layerGroup: node.layerGroup || ''
layerGroup: node.layerGroup || '',
parentW: parentW || rw
});
}
// 保留 raster 节点作为图片
if (node.type === 'RECTANGLE' && node.layerGroup === 'comparison' && node.backgroundImages && node.backgroundImages.length > 0) {
results.push({
id: node.id, type: 'IMAGE', name: node.name, tag: 'RASTER',
rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh },
styles: node.styles || {}, src: '',
layerGroup: node.layerGroup || '',
parentW: parentW || rw,
backgroundImages: node.backgroundImages
});
}
if (node.children && Array.isArray(node.children)) {
for (const child of node.children) {
results.push(...collectSlideChildren(child, slideX, slideY));
results.push(...collectSlideChildren(child, slideX, slideY, rw));
}
}
return results;
@@ -335,13 +348,62 @@ function convert(input) {
}
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素`));
// ===== fallback: 收集不在 slide 子树内但坐标在 slide 范围内的节点 =====
// 从根节点收集所有非 slide 节点
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.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 },
styles: node.styles || {}, src: node.src || node.text || '',
layerGroup: node.layerGroup || ''
});
}
if (node.children) for (const c of node.children) {
// 跳过 slide 容器的子树(它们已经被 collectSlideChildren 处理了)
if (c.name?.startsWith('slide-')) continue;
results.push(...collectOrphans(c));
}
return results;
}
const orphans = collectOrphans(inputRoot);
console.log(`游离节点: ${orphans.length}`);
// 把游离节点按 y 坐标归入最近的 slide
for (const or of orphans) {
if (or.rect.w == null || or.rect.h == null) continue;
let best = null;
let bestDist = Infinity;
for (const sg of slideGroups) {
const dist = Math.abs(or.rect.y - sg.slideRect.y);
if (dist < sg.slideRect.h && dist < bestDist) {
bestDist = dist;
best = sg;
}
}
if (best) {
// 转相对坐标
const relNode = { ...or, rect: { ...or.rect } };
relNode.rect.x = or.rect.x - best.slideRect.x;
relNode.rect.y = or.rect.y - best.slideRect.y;
best.children.push(relNode);
}
}
slideGroups.forEach(sg => console.log(` ${sg.name}: ${sg.children.length} 个元素 (含游离)`));
// ----------------------------------------------------------
// 第二步:每个 section → 一个 slide
// ----------------------------------------------------------
const slides = [];
for (const sg of slideGroups) {
const objects = [];
let objects = [];
let slideBg = null;
// 从 slide 容器节点取背景色
@@ -357,13 +419,13 @@ function convert(input) {
console.log('处理 ' + sg.name + ': ' + sg.children.length + ' 个节点');
for (const n of sg.children) {
// 跳过 raster 底图、纯容器
if (n.type === 'RECTANGLE' || n.layerGroup === 'comparison') { continue; }
// 跳过 raster 底图(保留 IMAGE 类型)
if (n.type === 'RECTANGLE') { continue; }
if (n.tag === 'BODY' || n.tag === 'HTML') { continue; }
const r = n.rect;
if (r == null || r.w == null || r.h == null) { continue; }
const S = 1/96;
const S = 1/72;
const opts = {
x: Math.round(r.x * S * 1000) / 1000,
y: Math.round(r.y * S * 1000) / 1000,
@@ -375,13 +437,18 @@ function convert(input) {
if (n.type === 'TEXT' && n.src) {
const st = n.styles;
const fs = parseFloat(st.fontSize) || 14;
opts.fontSize = Math.round(fs * 72 / 96);
var pt = Math.round(fs);
opts.fontSize = pt;
opts.fontFace = (st.fontFamily || 'Arial').split(',')[0].replace(/['"]/g,'').trim();
opts.color = rgbaToHex(st.color) || '000000';
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;
// 宽度优先用父容器,否则用文字自身宽度,再加 20% 余量防止换行
var baseW = (n.parentW || r.w);
opts.w = Math.round(baseW * 1.2 / 72 * 1000) / 1000;
objects.push({
type: 'text',
text: n.src,
@@ -389,6 +456,21 @@ function convert(input) {
});
}
// ===== 图片节点 =====
if (n.type === 'IMAGE' && n.backgroundImages && n.backgroundImages.length > 0) {
var imgKey = n.backgroundImages[0];
var asset = ctx.assets[imgKey];
if (asset && asset.data) {
objects.push({
type: 'image',
options: {
x: opts.x, y: opts.y, w: opts.w, h: opts.h,
data: asset.data
}
});
}
}
// ===== 提取填充色 =====
let fillColor = null;
const bg = n.styles.backgroundColor;
@@ -406,8 +488,13 @@ function convert(input) {
var shapeOpts = { x: opts.x, y: opts.y, w: opts.w, h: opts.h, fill: { color: fillColor } };
// 圆角
if (n.styles.borderTopLeftRadius && n.styles.borderTopLeftRadius !== '0px') {
shapeOpts.rectRadius = parseFloat(n.styles.borderTopLeftRadius) / 96;
var brVal = n.styles.borderTopLeftRadius;
if (brVal && brVal !== '0px') {
if (brVal.includes('%')) {
shapeOpts.rectRadius = parseFloat(brVal) / 100;
} else {
shapeOpts.rectRadius = parseFloat(brVal) / 72;
}
}
// 边框
@@ -425,10 +512,17 @@ function convert(input) {
}
}
objects.push({ type: 'shape', shapeName: 'rect', options: shapeOpts });
// 选择形状类型:圆角 >= 50% 且 w≈h 时用 ellipse(正圆),否则用 roundRect
var useShape = 'rect';
if (shapeOpts.rectRadius) {
var brPct = parseFloat(n.styles.borderTopLeftRadius);
var isCircle = (brPct >= 50 || (brPct.toString().includes('%') && parseFloat(brPct) >= 50)) && Math.abs(opts.w - opts.h) < 0.05;
useShape = isCircle ? 'ellipse' : 'roundRect';
}
objects.push({ type: 'shape', shapeName: useShape, options: shapeOpts });
}
// ===== 提取四周边框线(非填充) =====
// ===== 提取四周边框线 =====
var sides = [
{ key: 'borderTopWidth', yOff: 0, hOff: 0 },
{ key: 'borderBottomWidth', yOff: 1, hOff: 0 },
@@ -454,6 +548,38 @@ function convert(input) {
const slideData = { objects };
if (slideBg) slideData.background = slideBg;
// 提取 slide 容器自身的四周边框
if (slideContainer && slideContainer.styles) {
var sides = [
{ key: 'borderTopWidth', keyC: 'borderTopColor', keyS: 'borderTopStyle', yOff: 0, xOff: 0, isW: false },
{ key: 'borderBottomWidth', keyC: 'borderBottomColor', keyS: 'borderBottomStyle', yOff: 1, xOff: 0, isW: false },
{ key: 'borderLeftWidth', keyC: 'borderLeftColor', keyS: 'borderLeftStyle', xOff: 0, yOff: 0, isW: true },
{ key: 'borderRightWidth', keyC: 'borderRightColor', keyS: 'borderRightStyle', xOff: 1, yOff: 0, isW: true }
];
for (var si = 0; si < sides.length; si++) {
var side = sides[si];
var sw = parseFloat(slideContainer.styles[side.key]);
var sc = rgbaToHex(slideContainer.styles[side.keyC]);
if (sw > 0 && sc && slideContainer.styles[side.keyS] !== 'none') {
var sr = sg.slideRect;
var lineX = 0, lineY = 0, lineW = 0, lineH = 0;
if (side.isW) {
lineW = sw / 72;
lineH = sr.h / 72;
lineX = side.xOff === 0 ? 0 : (sr.w / 72 - sw/72);
lineY = 0;
} else {
lineW = sr.w / 72;
lineH = sw / 72;
lineX = 0;
lineY = side.yOff === 0 ? 0 : (sr.h / 72 - sw/72);
}
slideData.objects.push({ type: 'shape', shapeName: 'rect', options: { x: lineX, y: lineY, w: lineW, h: lineH, fill: { color: sc } } });
}
}
}
slides.push(slideData);
}
@@ -465,8 +591,8 @@ function convert(input) {
if (slideGroups.length > 0) {
const sr = slideGroups[0].slideRect;
if (sr && sr.w && sr.h) {
const sw = sr.w / 96;
const sh = sr.h / 96;
const sw = sr.w / 72;
const sh = sr.h / 72;
layout = 'CUSTOM';
return {
presentation: { layout: 'CUSTOM', slideWidth: sw, slideHeight: sh },
@@ -549,6 +675,11 @@ if (require.main === module) {
x: o.x, y: o.y, w: o.w, h: o.h,
fill: o.fill ? { color: o.fill.color } : undefined
});
} else if (obj.type === 'image') {
slide.addImage({
x: o.x, y: o.y, w: o.w, h: o.h,
data: o.data || o.path
});
}
}
}