VSTO中Word转换Range为Image的方法
VSTO中Word转换Range为Image的方法
前言
是一套用于创建自定义Office应用程序的Visual Studio工具包,通过Interop提供的增强Office对象,可以对Word文档进行编程操作。是Word中执行操作的一个单元,可以理解成文档中一个选中的部分或者区域,针对这个选中部分,可以应用格式、修改文字和颜色等功能。出于各种业务的需求,常常需要将Word文档或者Word文档的一部分变成图片。本文对常见的Range转换为Image的方法进行了讨论和分析,并提出一些改良的地方。
剪贴板作为中介
原理很简单,就是将Range.Copy()到剪贴板中,剪贴板是的一种方式,然后从剪贴板中获取metafile的信息还原成一个图片。
//原理示意 //source为Word.Range对象 //range.Copy()会将range中的内容复制到剪贴板中。 source.Copy(); //从剪贴板中获取metafile的数据 var metaData = GetMetaFileFromCicpBoard(); //根据metafile的信息构建出一个图片对象 Image image = GetImageFromMetaData(metaData);
这种方法在网上很常见,效果和手动在Word文档中复制一段文字,然后粘贴在<画图>软件上一样。然而在程序中,这个效果比较差。实际过程中,从剪贴板中获取数据,经常会失败,无法获取数据,推测是其他进程操作了剪贴板,数据丢失了。而且解析剪贴板的数据,需要引入的库已经很多年没维护了,经常出异常。
通过EnhMetaFileBits生成图片
这是最正常的方法了,直接通过API生成图片,效果好,速度快,图片的内容就是Word文档中的显示内容。
/// <summary> /// 获取原生的range变为图片的方式 /// </summary> /// <param name="range">选中部分</param> /// <returns>图片,Image对象</returns> public static Image GetRangeRealImage(Word.Range range) { if (range == null) { throw new Exception("range is empty"); } var bits = (byte[])range.EnhMetaFileBits; System.IO.MemoryStream ms = new System.IO.MemoryStream(bits); var ret = Image.FromStream(ms); return ret; }
在实际使用过程中,发现这种方法也存在缺陷。在不足一页的range中,可以显示全部内容;在多页的range中,生成的图片只能显示第一页的内容。
获取多页Range的图片
为了解决这个问题,一个通常的思路是:既然可以获取一页的图片,把多页分成一页一页的,获取每一页的图片,然后合并起来,就是多页内容的图片了。
//伪代码 /// <summary> /// 将range转换为完全的image图片 /// </summary> /// <param name="range">选中部分</param> /// <returns>图片</returns> public Image RangeToImage(Word.Range range) { var ret = RangeImage.GetRangeRealImage(range); //如果图片高度小于一页,直接返回 var limitHeight = 2000; if (ret.Height < limitHeight) { return ret; } //可能大于一页 return GetBigRangePic(range); }
从range中按页切割其实很困难,然而页是Word文档中的一个单位。可以把range复制到一个新文档B中,获取B文档的页数,获取B文档的每一页,然后就可以获取每一页的range部分了。
//伪代码 /// <summary> /// 一种生成range大图的方法。word中range只能显示一页的区域, /// range超过一页,也只能显示一页。 /// 将range复制到新文档中,拼接文档中 /// 每一页的图片,可以生成range的全图。 /// </summary> /// <param name="source">需要生成图片的range</param> /// <returns>生成好的image对象</returns> public Image GetBigRangePic(Word.Range source) { Image ret = null; var wordDoc = newTmpDoc(); //将source的内容复制的新文档中 addRangeToDoc(wordDoc, source); //获取新文档的页数 var num = wordDoc.ComputeStatistics(Word.WdStatistic.wdStatisticPages); //获取每一页的图片,并合并 for (int i= 1; i<=num; i++) { object page_num = i; wordDoc.Application.Selection.GoTo(Word.WdGoToItem.wdGoToPage, Word.WdGoToDirection.wdGoToAbsolute, num, page_num); var pageRange = wordDoc.Application.Selection.Bookmarks[@"\Page"].Range; var pageImage = RangeImage.GetRangeRealImage(pageRange); //第一页等于图片,第二页以后合并前面的部分 if (ret == null) { ret = pageImage; }else { ret = MergeImageVertical(ret, pageImage); } } //手动GC GC.Collect(); //保证线程安全 lock (objlock) { //关闭文档 try { object saveChange = Word.WdSaveOptions.wdDoNotSaveChanges; wordDoc.Close(ref saveChange, ref Nothing, ref Nothing); } catch (System.Runtime.InteropServices.COMException e) { Log.GetInstance().Error("close err:" + e.Message); } } return ret; }
这种方式成功获取了多页range的图片。其缺点在于是合并多页的图片,多页间的部分合并了看起来起来不自然;还有合并多页的图片,转换为多个Bitmap,然后开始合并,cpu负载高,消耗的内存大。
range替换为image的shape
将特定部分的range直接替换成一个图片,并删除原内容。打个比方,就像是魔术表演中,魔术师把手中的帽子变成兔子一样。
由于在Word中插入一个图片,必须是本地文件系统中的图片文件,range获取到的图片必须先存储在本地的临时目录,然后再插入到Word的相应位置中。
//处理流程 Range->图片->保存在本地,以png格式->插入到文档中->删除原先range的内容
//伪代码 //range为Word.Range对象 //获取图片 var image = RangeImage.GetRangeRealImage(range); //保存图片到本地 var FileName = Config.GetInstance().Get("runtimePath") + System.IO.Path.DirectorySeparatorChar + "tmpTable.png"; image.Save(FileName, System.Drawing.Imaging.ImageFormat.Png); //在原位置插入图片 object SaveWithDocument = true; object LinkToFile = false; var wordDoc = range.Document; object Anchor = wordDoc.Range(range.Start); var t = wordDoc.Application.ActiveDocument.InlineShapes.AddPicture(FileName, ref LinkToFile, ref SaveWithDocument, ref Anchor); //删除原内容 range.Delete();