feat: TEXT 节点合并 — 同 y 坐标的内联文本合并为富文本
- 同 y(误差<5px)且 x 相邻或重叠的 TEXT 节点合并 - 合并后保留 bold/italic/color 等格式差异 - 用 pptxgenjs 原生富文本支持渲染 - 16个合并对象,不影响其他独立文本框
This commit is contained in:
+76
-5
@@ -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) {
|
for (const n of sg.children) {
|
||||||
// 跳过 raster 底图(保留 IMAGE 类型)
|
// 跳过 raster 底图(保留 IMAGE 类型)
|
||||||
if (n.type === 'RECTANGLE') { continue; }
|
if (n.type === 'RECTANGLE') { continue; }
|
||||||
@@ -541,10 +599,10 @@ function convert(input) {
|
|||||||
if (st.textDecorationLine.includes('underline')) opts.underline = true;
|
if (st.textDecorationLine.includes('underline')) opts.underline = true;
|
||||||
if (st.textDecorationLine.includes('line-through')) opts.strike = 'sngStrike';
|
if (st.textDecorationLine.includes('line-through')) opts.strike = 'sngStrike';
|
||||||
}
|
}
|
||||||
// 字符间距
|
// 字符间距(CSS letterSpacing px → pptxgenjs charSpacing,单位 pt)
|
||||||
if (st.letterSpacing && st.letterSpacing !== 'normal') {
|
if (st.letterSpacing && st.letterSpacing !== 'normal') {
|
||||||
var ls = parseFloat(st.letterSpacing);
|
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) {
|
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 * 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
|
||||||
|
// 补偿 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 内容区右边界
|
// 确保 x + w 不超出 slide 内容区右边界
|
||||||
var maxW = contentW - r.x;
|
var maxW = contentW - r.x;
|
||||||
if (maxW < 108) maxW = 108;
|
if (maxW < 108) maxW = 108;
|
||||||
@@ -572,7 +635,7 @@ function convert(input) {
|
|||||||
|
|
||||||
objects.push({
|
objects.push({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: n.src,
|
text: n._richText || n.src, // 富文本数组或普通字符串
|
||||||
options: opts
|
options: opts
|
||||||
});
|
});
|
||||||
continue; // TEXT 节点不参与后续的填充色/边框逻辑
|
continue; // TEXT 节点不参与后续的填充色/边框逻辑
|
||||||
@@ -626,12 +689,20 @@ function convert(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 边框:只在四边都有 border 时加 shape 的 line,单边由后续"四周边框线"逻辑处理
|
// 边框:只在四边颜色和宽度一致时加 shape 的 line,否则由"四周边框线"逻辑处理
|
||||||
var bw = parseFloat(n.styles.borderTopWidth);
|
var bw = parseFloat(n.styles.borderTopWidth);
|
||||||
var bbw = parseFloat(n.styles.borderBottomWidth);
|
var bbw = parseFloat(n.styles.borderBottomWidth);
|
||||||
var blw = parseFloat(n.styles.borderLeftWidth);
|
var blw = parseFloat(n.styles.borderLeftWidth);
|
||||||
var brw = parseFloat(n.styles.borderRightWidth);
|
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);
|
var bc = rgbaToHex(n.styles.borderTopColor);
|
||||||
if (bc) shapeOpts.line = { color: bc, width: bw };
|
if (bc) shapeOpts.line = { color: bc, width: bw };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user