Vue导出Html为Word中包含图片在Microsoft Word显示异常问题
问题背景
碰到一个问题:将包含图片和SVG数学公式的HTML内容导出为Word文档时,将图片都转为ase64格式导出,在WPS Word中显示正常,但是在Microsoft Word中出现图片示异常。
具体问题表现
- WPS兼容性:在WPS中显示正常,说明是Microsoft Word特有的兼容性问题
- SVG数学公式:在Word中显示为"当前无法显示此图片"
- 普通图片:显示不正常或无法显示
技术方案设计
三重兜底机制
我们设计了一个三层处理机制,确保在各种情况下都能提供最佳的用户体验:
第一层:前端Canvas转Base64
const imageUrlToWordCompatibleBase64 = async (imageUrl: string): Promise<string> => {return new Promise((resolve, reject) => {try {const img = new Image();img.onload = () => {try {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');if (!ctx) {reject(new Error('can not create canvas context'));return;}// 设置Canvas尺寸canvas.width = img.width;canvas.height = img.height;// 设置白色背景ctx.fillStyle = 'white';ctx.fillRect(0, 0, canvas.width, canvas.height);// 绘制图片ctx.drawImage(img, 0, 0);// 转换为PNG base64,使用更兼容的格式const base64 = canvas.toDataURL('image/png, 0.9');resolve(base64);} catch (error) {reject(error);}};img.onerror = () => {reject(new Error(`image load failed: ${imageUrl}`));};img.src = imageUrl;} catch (error) {reject(error);}});
};
第二层:后端代理转Base64
const getImageBase64ViaProxy = async ({imageUrl, imageId}: {imageUrl: string, imageId: number}): Promise<string> => {try {// 添加随机延迟避免缓存const randomDelay = Math.random() * 2000 + 1000;await new Promise(resolve => setTimeout(resolve, randomDelay));const response = await adminApi.getImageBase64Api({ imageId: imageId,imagePath: imageUrl,});const data = response.data;if (!data) {throw new Error('image base64 is null');}// 处理后端返回的新结构 { imageId: string, imageBase64: string }if (data && typeof data === 'object' && 'imageId' in data && 'imageBase64' in data) {const imageBase64 = data.imageBase64;if (typeof imageBase64 === 'string') {if (imageBase64.startsWith('data:image/')) {return imageBase64;} else {return `data:image/png;base64,${imageBase64}`;}}}// 兼容旧格式:如果后端返回的是base64字符串,直接返回if (typeof data === 'string') {const dataStr = data as string;if (dataStr.startsWith('data:image/')) {return dataStr;} else {return `data:image/png;base64,${dataStr}`;}}throw new Error('Unsupported data format from backend');} catch (error) {console.error(`处理图片失败 ImageId: ${imageId}`, error);throw error;}
};
第三层:降级为Alt提示
当图片处理失败时,提供一个友好的降级方案:
// 降级为alt提示
imgElement.style.maxWidth = '100%';
imgElement.style.height = 'auto';
imgElement.style.display = 'inline-block';
imgElement.style.margin = '8px 0';
imgElement.style.borderRadius = '4px';
imgElement.style.border = '1px solid #ddd';
imgElement.style.padding = '4px';
imgElement.style.backgroundColor = '#f9f9f9';
if (!imgElement.alt) {imgElement.alt = `image: ${src}`;
}
SVG数学公式特殊处理
针对SVG数学公式,我们设计了专门的转换方案:
const svgToWordCompatiblePng = async (svgElement: SVGElement, width: number, height: number): Promise<string> => {return new Promise((resolve, reject) => {try {// 克隆SVG元素const clonedSvg = svgElement.cloneNode(true) as SVGElement;// 设置SVG尺寸clonedSvg.setAttribute('width', width.toString());clonedSvg.setAttribute('height', height.toString());// 设置viewBox以保持比例if (!clonedSvg.getAttribute('viewBox')) {clonedSvg.setAttribute('viewBox', `0 0 ${width} ${height}`);}// 序列化SVGconst serializer = new XMLSerializer();const svgString = serializer.serializeToString(clonedSvg);// 创建SVG的data URLconst svgDataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));// 创建Image对象const img = new Image();img.onload = () => {try {// 创建Canvasconst canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');if (!ctx) {reject(new Error('can not create canvas context'));return;}// 设置Canvas尺寸canvas.width = width;canvas.height = height;// 设置白色背景(确保透明度问题)ctx.fillStyle = 'white';ctx.fillRect(0, 0, width, height);// 绘制图片到Canvasctx.drawImage(img, 0, 0, width, height);// 转换为PNG base64,使用更兼容的格式const pngBase64 = canvas.toDataURL('image/png, 0.9');resolve(pngBase64);} catch (error) {reject(error);}};img.onerror = () => {reject(new Error('image load failed'));};img.src = svgDataUrl;} catch (error) {reject(error);}});
};
并发处理优化
问题分析
在初期实现中,我们遇到了并发请求导致的重复处理问题。相同imageId被请求了两次,导致资源浪费和性能问题。
解决方案
- 串行处理:将图片处理改为串行处理,避免并发问题
- 随机延迟:为每个请求添加随机延迟,避免缓存和并发冲突
- 重复检测:添加已处理imageId集合,避免重复请求
// 串行处理图片,避免并发问题
const results: Array<{success: boolean;method: string;index: number;imgElement: HTMLImageElement;base64?: string;error?: string;
}> = [];for (let i = 0; i < imageProcessingTasks.length; i++) {const task = imageProcessingTasks[i];const { imgElement, src, index, originalWidth, originalHeight } = task;try {// 1. 尝试前端canvas转base64try {const base64 = await imageUrlToWordCompatibleBase64(src);imgElement.src = base64;results.push({ success: true, method: 'canvas', index, imgElement, base64 });} catch (error: any) {// 2. 尝试后端代理try {const base64 = await getImageBase64ViaProxy({ imageUrl: src, imageId: index});imgElement.src = base64;results.push({ success: true, method: 'proxy', index, imgElement, base64 });} catch (proxyError: any) {// 3. 降级为alt提示// ... 降级处理代码}}} catch (error: any) {console.error(`Error processing image ${index + 1}:`, error);results.push({ success: false, method: 'error', index, imgElement, error: error?.message || error });}
}
重复Base64检测和修复
问题识别
我们发现相同图片可能产生相同的base64结果,这可能导致Word中的显示问题。
解决方案
// 检查是否有重复的base64结果
const base64Results = results.filter(r => r.success && r.method === 'proxy').map(r => r.base64).filter(Boolean);const uniqueBase64 = new Set(base64Results);
if (uniqueBase64.size !== base64Results.length) {// 详细分析重复的base64const base64Count = new Map<string, number>();base64Results.forEach((base64) => {if (base64) {base64Count.set(base64, (base64Count.get(base64) || 0) + 1);}});// 尝试修复重复问题:为重复的图片重新请求const duplicateTasks: Array<{imgElement: HTMLImageElement;src: string;index: number;originalWidth: number;originalHeight: number;originalResult: any;needsReprocessing: boolean;}> = [];// 记录已经处理过的imageId,避免重复处理const processedImageIds = new Set<number>();base64Count.forEach((count, base64) => {if (count > 1) {const duplicateResults = results.filter(r => r.success && r.method === 'proxy' && r.base64 === base64).map((r, idx) => ({ result: r, taskIndex: idx })).filter(({ result, taskIndex }) => {const task = imageProcessingTasks[taskIndex];return task !== undefined;});// 保留第一个,其余的需要重新处理duplicateResults.slice(1).forEach(({ result, taskIndex }) => {const task = imageProcessingTasks[taskIndex];if (task && !processedImageIds.has(task.index)) {processedImageIds.add(task.index);duplicateTasks.push({...task,originalResult: result,needsReprocessing: true});}});}});if (duplicateTasks.length > 0) {// 重新处理重复的图片,添加随机延迟避免并发问题for (const duplicateTask of duplicateTasks) {try {// 添加随机延迟await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));const newBase64 = await getImageBase64ViaProxy({ imageUrl: duplicateTask.src, imageId: duplicateTask.index });// 更新对应的img元素const imgElement = duplicateTask.imgElement;if (imgElement && newBase64 !== duplicateTask.originalResult.base64) {imgElement.src = newBase64;}} catch (error) {console.error(`重新处理失败: ImageId ${duplicateTask.index}`, error);}}}
}
Word模板优化
为了确保在Microsoft Word中的最佳显示效果,我们设计了专门的HTML模板:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible content=IE=edge"><style>body {font-family: 'Times New Roman', Times, serif; /* Microsoft Word默认字体 */line-height: 1.5;font-size: 12pt; /* Word默认字号 */margin: 0;padding: 20px;}img { max-width: 100%; height: auto; display: inline-block;vertical-align: middle;margin: 4px;}/* Microsoft Word兼容的图片样式 */img[src^="data:image/"] {border: none;outline: none;}/* 确保图片在Word中正确显示 */.word-image {display: inline-block;vertical-align: middle;margin: 4px;}</style>
</head>
<body><!-- 内容占位符 -->
</body>
</html>
导出功能实现
主要导出函数
export const exportDocx = async (className: string,title = 'document',type = 'docx'
): Promise<void> => {const baseTemplate = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible content=IE=edge"><style>body {font-family: Times New Roman', Times, serif;line-height: 1.5;font-size: 12pt;margin: 0;padding: 20px;}img { max-width: 100%; height: auto; display: inline-block;vertical-align: middle;margin: 4px;}img[src^="data:image/"] {border: none;outline: none;}.word-image {display: inline-block;vertical-align: middle;margin: 4px;}</style></head><body>${getHTMLContentByClassName(className)}</body></html>`;const htmlSvgContent = await handleSvgToBase64(baseTemplate);try {const options = {orientation: 'portrait',margins: { top: 720, right: 720, bottom: 720, left: 720 }, // Word默认边距header: false,footer: false,pageSize: 'A4'};const data = await asBlob(htmlSvgContent, options as any);const fileName = `${title.replace(/[<>:"/\\|?*]/g, '')}-${Date.now()}.${type}`; // 移除非法字符saveAs(data as Blob, fileName);} catch (error) {console.error('export docx error:', error);}
};
关键注意事项
1. 图片格式兼容性
- PNG格式:Microsoft Word对PNG格式支持最好
- Base64编码:确保使用正确的MIME类型前缀
- 白色背景:为透明图片设置白色背景,避免显示问题
2. 并发处理
- 串行处理:避免并发请求导致的重复处理
- 随机延迟:防止缓存和并发冲突
- 重复检测:识别并修复重复的base64结果
3. 错误处
- 三层兜底:确保在各种情况下都有降级方案
- 详细日志:记录处理过程,便于调试
- 用户友好:提供清晰的错误提示
最终效果
经过优化后,我们的解决方案实现了:
测试结果
- ✅ Microsoft Word 2016/2019/365:图片正常显示
- ✅ WPS Office:完全兼容
- ✅ 数学公式:SVG转PNG后正常显示
- ✅ 复杂布局:保持原有格式和样式
总结
这个方案成功解决了Microsoft Word导出中的图片显示问题。