|
# PPT矢量元素导出问题修复方案 |
|
|
|
## 问题分析 |
|
|
|
通过代码分析,发现PPT内置矢量元素无法正常导出的主要原因: |
|
|
|
### 1. 核心问题 |
|
在 `useExport.ts` 第977行存在一个临时处理逻辑: |
|
```typescript |
|
if (svgRef.clientWidth < 1 || svgRef.clientHeight < 1) continue // 临时处理(导入PPTX文件带来的异常数据) |
|
``` |
|
|
|
这个检查会跳过所有尺寸小于1像素的SVG元素,但某些矢量元素在特定情况下可能确实会出现这种情况。 |
|
|
|
### 2. 相关问题 |
|
- SVG元素在DOM中可能未完全渲染 |
|
- `vector-effect="non-scaling-stroke"` 属性可能影响尺寸计算 |
|
- 特殊形状元素(special=true)的DOM查询可能失败 |
|
- SVG序列化过程缺乏错误处理 |
|
|
|
## 修复方案 |
|
|
|
### 方案1: 改进尺寸检查逻辑 |
|
|
|
```typescript |
|
// 替换原有的简单尺寸检查 |
|
if (el.special) { |
|
const svgRef = document.querySelector(`.thumbnail-list .base-element-${el.id} svg`) as HTMLElement |
|
|
|
// 改进的尺寸检查 |
|
if (!svgRef) { |
|
console.warn(`SVG element not found for shape ${el.id}`); |
|
continue; |
|
} |
|
|
|
// 获取多种尺寸信息进行判断 |
|
const clientWidth = svgRef.clientWidth; |
|
const clientHeight = svgRef.clientHeight; |
|
const boundingRect = svgRef.getBoundingClientRect(); |
|
const computedStyle = window.getComputedStyle(svgRef); |
|
|
|
// 更智能的尺寸判断 |
|
const hasValidSize = ( |
|
(clientWidth > 0 && clientHeight > 0) || |
|
(boundingRect.width > 0 && boundingRect.height > 0) || |
|
(parseFloat(computedStyle.width) > 0 && parseFloat(computedStyle.height) > 0) |
|
); |
|
|
|
if (!hasValidSize) { |
|
console.warn(`Invalid SVG dimensions for shape ${el.id}:`, { |
|
clientWidth, |
|
clientHeight, |
|
boundingRect: { width: boundingRect.width, height: boundingRect.height }, |
|
computedStyle: { width: computedStyle.width, height: computedStyle.height } |
|
}); |
|
continue; |
|
} |
|
|
|
// SVG序列化with错误处理 |
|
let base64SVG; |
|
try { |
|
base64SVG = svg2Base64(svgRef); |
|
if (!base64SVG || base64SVG === 'data:image/svg+xml;base64,') { |
|
throw new Error('SVG serialization returned empty result'); |
|
} |
|
} catch (error) { |
|
console.error(`SVG serialization failed for shape ${el.id}:`, error); |
|
continue; |
|
} |
|
|
|
// 其余导出逻辑... |
|
} |
|
``` |
|
|
|
### 方案2: 添加渲染等待机制 |
|
|
|
```typescript |
|
// 在导出开始前添加渲染等待 |
|
const ensureElementsRendered = async () => { |
|
return new Promise<void>((resolve) => { |
|
// 强制重绘 |
|
document.body.offsetHeight; |
|
|
|
// 等待下一个动画帧 |
|
requestAnimationFrame(() => { |
|
requestAnimationFrame(() => { |
|
resolve(); |
|
}); |
|
}); |
|
}); |
|
}; |
|
|
|
// 在exportPPTX函数开始处调用 |
|
export const exportPPTX = async (slides: Slide[], title: string, ignoreMedia = false) => { |
|
exporting.value = true; |
|
|
|
// 确保所有元素已渲染 |
|
await ensureElementsRendered(); |
|
|
|
// 其余导出逻辑... |
|
} |
|
``` |
|
|
|
### 方案3: 改进SVG处理逻辑 |
|
|
|
```typescript |
|
// 改进svg2Base64函数 |
|
export const svg2Base64 = (element: Element) => { |
|
try { |
|
// 克隆元素以避免修改原始DOM |
|
const clonedElement = element.cloneNode(true) as Element; |
|
|
|
// 确保SVG有正确的命名空间 |
|
if (clonedElement.tagName.toLowerCase() === 'svg') { |
|
clonedElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); |
|
clonedElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); |
|
} |
|
|
|
const XMLS = new XMLSerializer(); |
|
const svg = XMLS.serializeToString(clonedElement); |
|
|
|
if (!svg || svg.length === 0) { |
|
throw new Error('SVG serialization returned empty string'); |
|
} |
|
|
|
const encoded = encode(svg); |
|
if (!encoded) { |
|
throw new Error('Base64 encoding failed'); |
|
} |
|
|
|
return PREFIX + encoded; |
|
} catch (error) { |
|
console.error('svg2Base64 failed:', error); |
|
throw error; |
|
} |
|
}; |
|
``` |
|
|
|
### 方案4: 添加降级处理 |
|
|
|
```typescript |
|
// 为特殊形状添加降级处理 |
|
else if (el.type === 'shape') { |
|
if (el.special) { |
|
let base64SVG; |
|
|
|
try { |
|
// 尝试从DOM获取SVG |
|
const svgRef = document.querySelector(`.thumbnail-list .base-element-${el.id} svg`) as HTMLElement; |
|
if (svgRef && svgRef.clientWidth > 0 && svgRef.clientHeight > 0) { |
|
base64SVG = svg2Base64(svgRef); |
|
} else { |
|
throw new Error('SVG element not found or has invalid dimensions'); |
|
} |
|
} catch (error) { |
|
console.warn(`Failed to export special shape ${el.id} as SVG, falling back to path-based export:`, error); |
|
|
|
// 降级到普通形状处理 |
|
const scale = { |
|
x: el.width / el.viewBox[0], |
|
y: el.height / el.viewBox[1], |
|
}; |
|
const points = formatPoints(toPoints(el.path), scale); |
|
|
|
let fillColor = formatColor(el.fill); |
|
if (el.gradient) { |
|
const colors = el.gradient.colors; |
|
const color1 = colors[0].color; |
|
const color2 = colors[colors.length - 1].color; |
|
const color = tinycolor.mix(color1, color2).toHexString(); |
|
fillColor = formatColor(color); |
|
} |
|
|
|
const opacity = el.opacity === undefined ? 1 : el.opacity; |
|
|
|
const options: pptxgen.ShapeProps = { |
|
x: el.left / ratioPx2Inch.value, |
|
y: el.top / ratioPx2Inch.value, |
|
w: el.width / ratioPx2Inch.value, |
|
h: el.height / ratioPx2Inch.value, |
|
fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, |
|
points, |
|
}; |
|
|
|
if (el.flipH) options.flipH = el.flipH; |
|
if (el.flipV) options.flipV = el.flipV; |
|
if (el.shadow) options.shadow = getShadowOption(el.shadow); |
|
if (el.outline?.width) options.line = getOutlineOption(el.outline); |
|
if (el.rotate) options.rotate = el.rotate; |
|
|
|
pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options); |
|
continue; |
|
} |
|
|
|
// 成功获取SVG的情况 |
|
if (base64SVG) { |
|
const options: pptxgen.ImageProps = { |
|
data: base64SVG, |
|
x: el.left / ratioPx2Inch.value, |
|
y: el.top / ratioPx2Inch.value, |
|
w: el.width / ratioPx2Inch.value, |
|
h: el.height / ratioPx2Inch.value, |
|
}; |
|
|
|
if (el.rotate) options.rotate = el.rotate; |
|
if (el.flipH) options.flipH = el.flipH; |
|
if (el.flipV) options.flipV = el.flipV; |
|
if (el.link) { |
|
const linkOption = getLinkOption(el.link); |
|
if (linkOption) options.hyperlink = linkOption; |
|
} |
|
|
|
pptxSlide.addImage(options); |
|
} |
|
} |
|
// 普通形状处理逻辑保持不变 |
|
else { |
|
// 现有的普通形状处理代码... |
|
} |
|
} |
|
``` |
|
|
|
## 实施步骤 |
|
|
|
1. **立即修复**: 实施方案1,改进尺寸检查逻辑 |
|
2. **增强稳定性**: 实施方案2,添加渲染等待机制 |
|
3. **提升兼容性**: 实施方案3,改进SVG处理 |
|
4. **添加容错**: 实施方案4,添加降级处理 |
|
|
|
## 测试验证 |
|
|
|
使用提供的 `debug_vector_export.html` 文件进行测试: |
|
1. 打开调试页面 |
|
2. 运行所有测试 |
|
3. 检查SVG序列化和Base64转换是否正常 |
|
4. 验证元素尺寸检测逻辑 |
|
|
|
## 预期效果 |
|
|
|
修复后应该能够: |
|
- 正确导出所有类型的矢量元素 |
|
- 提供详细的错误日志用于问题排查 |
|
- 在特殊情况下提供降级处理方案 |
|
- 提高导出成功率和稳定性 |