問題背景
碰到一個問題:將包含圖片和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導出中的圖片顯示問題。