关于NPOI的两个坑——保存到MemoryStream自动关闭和AddPicture生成的文件打不开的问题
今天踩了两个NPOI的坑,一个比一个气人。
故事背景都是要导出Excel或者Word。
1. 导出Excel,写入Stream被关闭
很普通的导出Excel,于是后端先去数据库里查数据,然后用NPOI生成Excel表格,把生成的结果传给前端,触发浏览器下载保存到本地。
按照这个思路,我们很快写出了代码,然而却发现导出存在问题!出问题的代码是这样的:
1 | using var ms = new MemoryStream(); |
结果运行一下,喜提异常一个:System.ObjectDisposedException: Cannot access a closed Stream.
我就想,我好好的一个Stream,怎么就Closed了呢?
一般我们在C#中使用Stream,原则是谁打开的,就谁负责关闭。这也是为什么我们要写using var ms = new MemoryStream()
。由于using
关键字的特性,这里打开的Stream会在结束这段代码时自动关闭并清理。
NPOI.XSSF 这个Excel组件可好,他直接自作多情的在Write(ms)的调用里面把Stream关了。如果是写文件,写到这里关了倒无可厚非,反正文件已经写完了。
但是我们这里本地不想保存这个文件,而是希望直接把结果推给浏览器。也就是这个Stream流需要再读一遍的,你直接给关了肯定不行啊。
最后也没办法,就继承了一个MemoryStream,然后重载Close,不让它关掉。
定义了NpoiMemoryStream
,如下:
1 | public class NpoiMemoryStream : MemoryStream |
那么我们就把用法也对应改成:
修改后的代码:
1 | using var ms = new NpoiMemoryStream(); |
2. 往Word文档里添加图片,导出后的docx文件Word打不开
这个就更坑了,需求是导出工单为Word文档。那么在工单界面上传的图片需要用插图的形式插在Word文档里面。
那么我们正常写代码,就成了下面这样:
有问题的代码:
1 | using var imgStream = await efs.DownloadImage(imageUrl); |
写好了,运行,没报错,docx生成了。那么我们用Word打开一下:
噔噔咚!
1 | Word 在试图打开文件时遇到错误。 |
这是为什么呢?我尝试了很多可能,修改设定的长宽啊,不从网络下载图片而是先给个本地图片试试啊,给图片一个单独的Run啊,尝试把图片不再内联到文档里,而是插入成“四周型”啊,都没用。最后我确信了自己的代码没有问题,而问题出在NPOI.XWPF里面。
然后我去查看了生成的Word文档内部结构。发现了Bug的所在:
大家知道docx文件其实是一组xml的zip包。我们把生成的docx后缀名改成zip,解压,用VSCode打开,看看到底给我们生成了什么xml出来。
然后发现这个图片生成的XML是这样的:
1 | <wp:docPr name="Drawing 0" descr="微信-01.png.jpg"></wp:docPr> |
而我正常在Word里拖进去一张图片,生成的XML是这样的:
1 | <wp:docPr id="237795" name="Drawing 0" descr="微信-01.png.jpg"></wp:docPr> |
NPOI,你id呢?
只能用NPOI给出的内部工具手工解决一下:
修改后的代码:
1 | using var imgStream = await efs.DownloadImage(imageUrl); |
然后再尝试运行,生成的Word终于正常打开了。
以上两个问题感觉已经在NPOI中存在好多年了,然而并没有人修复过。目前也不知道java版的祖宗Apache POI是不是有同样的问题。但是碰到了的话确实很坑。