vue-office/excel Canvas绘制单元格图片概述vue-office/excel原作者https://github.com/501351981/vue-office扩展源码https://github.com/WhileDew/vue-office/tree/master 使用 HTML5 Canvas 技术在电子表格中绘制单元格图片支持两种图片类型普通媒体图片通过_media数组管理嵌入型 DISPIMG 公式图片通过DISPIMG(ID, index)公式定义核心文件文件路径功能描述src/x-spreadsheet/canvas/draw.jsCanvas绑定、绘图基础类src/media.js图片渲染核心逻辑src/stores/dispimgStore.js图片数据存储src/main.vue主组件整合渲染流程src/x-spreadsheet/core/data_proxy.js数据代理单元格位置计算架构设计1. Canvas绑定Draw 类(src/x-spreadsheet/canvas/draw.js)classDraw{constructor(el,width,height){this.elel;this.ctxel.getContext(2d);this.resize(width,height);this.ctx.scale(dpr(),dpr());}}关键工具函数dpr(): 获取设备像素比npx(px): 像素转换根据设备像素比缩放2. 图片数据存储dispimgStore(src/stores/dispimgStore.js)全局存储对象管理图片映射关系exportconstdispimgStore{formulaImageMap:null,// { ID_XXXX: 0, ID_YYYY: 1 }allSheetImages:null,// [ [img1, img2, ...], [sheet2Img1, ...] ]imageCell:{},// 图片单元格位置信息}3. Excel图片解析buildImageMap(src/media.js)从 Excel 文件中提取图片数据使用 JSZip 解压 Excel 文件读取xl/cellimages.xml获取图片ID映射加载xl/media/image*目录下的所有图片文件将图片数据存储到 dispimgStoreexportasyncfunctionbuildImageMap(excelBlobOrBuffer){returnJSZip.loadAsync(excelBlobOrBuffer).then(zip{constcellImagesXmlzip.file(xl/cellimages.xml);// 解析 XML构建 formulaImageMap// 加载所有图片文件构建 allSheetImages});}渲染流程1. 主渲染流程main.vue重写table.render方法在表格渲染后绘制图片lettableRenderxs.sheet.table.render;xs.sheet.table.renderfunction(...args){xsxs.sheettableRender.apply(xs.sheet.table,args);renderImageDebounce(ctx,mediasSource,workbookDataSource._worksheets[sheetIndex],offset,props.options);};2. 图片渲染函数renderImage(src/media.js)核心渲染函数分两步绘制第一步绘制普通媒体图片if(sheetsheet._media.length){sheet._media.forEach(media{let{imageId,range,type}media;letpositioncalcPosition(sheet,range,offset,options);if(typeimage){drawImage(ctx,imageId,medias[imageId],position);}});}第二步绘制嵌入型 DISPIMG 图片constformulaImageMapdispimgStore.getFormulaMap();constallSheetImagesdispimgStore.getAllSheetImages();for(letri1;rirowCount;ri){for(letci1;cicolCount;ci){constcellsheet.getCell(ri,ci);if(cell.textcell.text.startsWith(DISPIMG)){constmatchcell.text.match(/^DISPIMG\((.?),\s*(\d)\)/);if(match){constimageIdmatch[1];constmediaIndexformulaImageMap[imageId];constimgallSheetImages?.[0]?.[mediaIndex];constcellInfodispimgStore.getImageCell(imageId);// 加载并绘制图片保持比例、居中显示}}}}3. 位置计算calcPosition(src/media.js)计算图片在 Canvas 上的绘制位置functioncalcPosition(sheet,range,offset,options){// 计算基础 X 坐标考虑左侧序号列宽letbasicXclipWidth;for(leti0;inativeCol;i){basicXsheet?._columns?.[i]?.width*6||defaultColWidth;}// 计算基础 Y 坐标考虑顶部序号行高letbasicYclipHeight;for(leti0;inativeRow;i){basicYsheet?._rows?.[i]?.height||defaultRowHeight;}// 返回最终位置考虑滚动偏移和设备像素比return{x:(x-(offset?.scroll?.x||0))*devicePixelRatio,y:(y-(offset?.scroll?.y||0))*devicePixelRatio,width:width*devicePixelRatio,height:height*devicePixelRatio};}常量定义clipWidth 60: 左侧序号列宽clipHeight 25: 顶部序号行高defaultColWidth 80: 默认列宽defaultRowHeight 24: 默认行高4. 图片绘制drawImage(src/media.js)执行实际的 Canvas 绘制操作functiondrawImage(ctx,index,data,position){getImage(index,data).then(image{letsx0,sy0;letsWidthimage.width;letsHeightimage.height;letdxposition.x;letdyposition.y;letdWidthposition.width;letdHeightposition.height;// 处理裁剪当图片被序号列/行遮挡时if(dxclipWidth*devicePixelRatio){letdiffclipWidth*devicePixelRatio-dx;dxclipWidth*devicePixelRatio;dWidth-diff;sWidth-diff/scaleX;sxdiff/scaleX;}// 执行绘制letscalewindow.outerWidth/window.innerWidth;ctx.drawImage(image,sx,sy,sWidth,sHeight,dx*scale,dy*scale,dWidth*scale,dHeight*scale);});}5. 图片加载与缓存getImage(src/media.js)异步加载图片并缓存letcache[];functiongetImage(index,data){returnnewPromise(((resolve,reject){if(cache[index]){returnresolve(cache[index]);}const{buffer}data.buffer;letblobnewBlob([buffer],{type:image/data.extension});leturlURL.createObjectURL(blob);letimagenewImage();image.srcurl;image.onloadfunction(){resolve(image);cache[index]image;};}));}DISPIMG 图片渲染细节图片适配与居中嵌入型图片需要适配单元格大小并保持原始比例constimgRatioimgWidth/imgHeight;constcellRatiocellWidth/cellHeight;letdrawWidth,drawHeight;if(imgRatiocellRatio){drawWidthcellWidth;drawHeightdrawWidth/imgRatio;}else{drawHeightcellHeight;drawWidthdrawHeight*imgRatio;}// 居中绘制constoffsetXleft(cellWidth-drawWidth)/2;constoffsetYtop(cellHeight-drawHeight)/2;ctx.drawImage(image,offsetX*zoom,offsetY*zoom,drawWidth*zoom,drawHeight*zoom);单元格位置信息存储data_proxy.js在计算单元格位置时存储图片单元格信息if(cellcell.texttypeofcell.textstringcell.text.startsWith(DISPIMG)){constmatchcell.text.match(/^DISPIMG\((.?),\s*(\d)\)/);if(match){const[,imgId]match;dispimgStore.setImageCell(imgId,{left,top,width,height});}}技术要点1. 设备像素比处理所有尺寸计算都考虑设备像素比确保在高 DPI 屏幕上清晰显示constzoomwindow.devicePixelRatio||1;ctx.drawImage(image,offsetX*zoom,offsetY*zoom,drawWidth*zoom,drawHeight*zoom);2. 图片裁剪当图片位置超出可视区域时自动裁剪并调整绘制参数if(dxclipWidth*devicePixelRatio){letdiffclipWidth*devicePixelRatio-dx;dxclipWidth*devicePixelRatio;dWidth-diff;sWidth-diff/scaleX;sxdiff/scaleX;}3. 图片缓存机制使用全局缓存数组存储已加载的 Image 对象避免重复加载letcache[];if(cache[index]){returnresolve(cache[index]);}4. 滚动偏移处理计算图片位置时考虑滚动偏移量x:(x-(offset?.scroll?.x||0))*devicePixelRatio,y:(y-(offset?.scroll?.y||0))*devicePixelRatio,使用示例基本使用import{buildImageMap,renderImage}from./media;// 1. 加载 Excel 文件并构建图片映射awaitbuildImageMap(fileData);// 2. 获取 Canvas 上下文constcanvasrootRef.value.querySelector(canvas);constctxcanvas.getContext(2d);// 3. 渲染图片renderImage(ctx,mediasSource,sheet,offset,options);清除缓存import{clearCache}from./media;clearCache();性能优化建议图片缓存利用内置缓存机制避免重复加载防抖渲染使用renderImageDebounce避免频繁重绘按需加载只加载可视区域内的图片图片压缩在服务端对大图进行预处理依赖库jszip: 用于解压 Excel 文件DOMParser: 用于解析 XML 格式的 cellimages.xml注意事项图片 ID 映射从 Excel 的cellimages.xml中读取嵌入型图片使用DISPIMG(ID, index)公式定义所有尺寸计算都基于设备像素比图片绘制在表格渲染之后执行滚动时需要重新计算图片位置并重绘