From f4adac77c50563622eaf3cf67c3462ddc2ea8e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E8=BF=9B?= Date: Fri, 24 Jul 2026 17:16:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20TEXT=20=E8=8A=82=E7=82=B9=E5=90=88?= =?UTF-8?q?=E5=B9=B6=20=E2=80=94=20=E5=90=8C=20y=20=E5=9D=90=E6=A0=87?= =?UTF-8?q?=E7=9A=84=E5=86=85=E8=81=94=E6=96=87=E6=9C=AC=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E4=B8=BA=E5=AF=8C=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同 y(误差<5px)且 x 相邻或重叠的 TEXT 节点合并 - 合并后保留 bold/italic/color 等格式差异 - 用 pptxgenjs 原生富文本支持渲染 - 16个合并对象,不影响其他独立文本框 --- convert-w2p.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/convert-w2p.js b/convert-w2p.js index 67a1234..a4af2a6 100644 --- a/convert-w2p.js +++ b/convert-w2p.js @@ -507,6 +507,64 @@ function convert(input) { } } + // ===== TEXT 节点合并:同 y 坐标且 x 相邻的内联文本合并为富文本 ===== + const textNodes = sg.children.filter(n => n.type === 'TEXT' && n.src); + const merged = new Set(); // 已合并的节点 id + for (let i = 0; i < textNodes.length; i++) { + if (merged.has(textNodes[i].id)) continue; + const a = textNodes[i]; + const ra = a.rect; + // 找同 y 坐标(误差<5px)且 x 相邻(误差<5px)的节点 + const group = [a]; + for (let j = i + 1; j < textNodes.length; j++) { + if (merged.has(textNodes[j].id)) continue; + const b = textNodes[j]; + const rb = b.rect; + if (Math.abs(ra.y - rb.y) > 5) continue; + // 检查 x 相邻或重叠:a 的右边缘 ≈ b 的左边缘,或 x 坐标接近(内联元素嵌套) + const aRight = ra.x + ra.w; + const bRight = rb.x + rb.w; + const xAdjacent = Math.abs(aRight - rb.x) < 5 || Math.abs(bRight - ra.x) < 5; + const xOverlap = Math.abs(ra.x - rb.x) < 5; // 同 x 起始位置(strong 等内联元素) + if (xAdjacent || xOverlap) { + group.push(b); + merged.add(b.id); + } + } + if (group.length > 1) { + merged.add(a.id); + // 按 x 排序 + group.sort((m, n) => m.rect.x - n.rect.x); + // 创建富文本对象 + const minX = group[0].rect.x; + const maxRight = Math.max(...group.map(g => g.rect.x + g.rect.w)); + const minTop = Math.min(...group.map(g => g.rect.y)); + const maxBottom = Math.max(...group.map(g => g.rect.y + g.rect.h)); + const richText = group.map(g => ({ + text: g.src, + options: { + fontSize: Math.round(parseFloat(g.styles.fontSize) || 14), + fontFace: (g.styles.fontFamily || 'Arial').split(',')[0].replace(/['"]/g,'').trim(), + color: rgbaToHex(g.styles.color) || '000000', + bold: parseInt(g.styles.fontWeight) >= 700, + italic: g.styles.fontStyle === 'italic' + } + })); + sg.children.push({ + id: 'merged-' + a.id, + type: 'TEXT', + _richText: richText, + rect: { x: minX, y: minTop, w: maxRight - minX, h: maxBottom - minTop }, + styles: a.styles || {}, + src: group.map(g => g.src).join(''), + parentW: a.parentW, + _merged: true + }); + } + } + // 移除已合并的原始节点 + sg.children = sg.children.filter(n => !merged.has(n.id)); + for (const n of sg.children) { // 跳过 raster 底图(保留 IMAGE 类型) if (n.type === 'RECTANGLE') { continue; } @@ -541,10 +599,10 @@ function convert(input) { if (st.textDecorationLine.includes('underline')) opts.underline = true; if (st.textDecorationLine.includes('line-through')) opts.strike = 'sngStrike'; } - // 字符间距 + // 字符间距(CSS letterSpacing px → pptxgenjs charSpacing,单位 pt) if (st.letterSpacing && st.letterSpacing !== 'normal') { var ls = parseFloat(st.letterSpacing); - if (!isNaN(ls) && ls !== 0) opts.charSpacing = Math.round(ls * 100 / fs); // 转为百分比 + if (!isNaN(ls) && ls !== 0) opts.charSpacing = Math.round(ls * 72 / 96); // 96dpi px → 72dpi pt } // 透明度 if (st.opacity !== undefined && st.opacity !== '' && parseFloat(st.opacity) < 1) { @@ -564,6 +622,11 @@ function convert(input) { if (textW > contentW * 0.8) textW = contentW; if (textW > contentW) textW = contentW; if (textW < 108) textW = 108; // 最小 1.5in = 108px + // 补偿 PPT 文本框内部 padding(约0.2in = 14px) + textW = textW + 14; + // 如果估算的文字宽度超过文本框,扩展文本框 + var estTextW = n.src.length * pt * 0.65; // 粗略估算 + if (estTextW > textW && estTextW < contentW) textW = Math.ceil(estTextW); // 确保 x + w 不超出 slide 内容区右边界 var maxW = contentW - r.x; if (maxW < 108) maxW = 108; @@ -572,7 +635,7 @@ function convert(input) { objects.push({ type: 'text', - text: n.src, + text: n._richText || n.src, // 富文本数组或普通字符串 options: opts }); continue; // TEXT 节点不参与后续的填充色/边框逻辑 @@ -626,12 +689,20 @@ function convert(input) { } } - // 边框:只在四边都有 border 时加 shape 的 line,单边由后续"四周边框线"逻辑处理 + // 边框:只在四边颜色和宽度一致时加 shape 的 line,否则由"四周边框线"逻辑处理 var bw = parseFloat(n.styles.borderTopWidth); 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); + var bbc = rgbaToHex(n.styles.borderBottomColor); + var blc = rgbaToHex(n.styles.borderLeftColor); + var brc = rgbaToHex(n.styles.borderRightColor); + var allSameBorder = bw > 0 && bbw > 0 && blw > 0 && brw > 0 + && n.styles.borderTopStyle !== 'none' + && bw === bbw && bw === blw && bw === brw + && bc && bc === bbc && bc === blc && bc === brc; + if (allSameBorder) { var bc = rgbaToHex(n.styles.borderTopColor); if (bc) shapeOpts.line = { color: bc, width: bw }; }