前言
VSTO https://en.wikipedia.org/wiki/Visual_Studio_Tools_for_Office 是一套用于创建自定义 Office 应用程序的 Visual Studio 工具包, 通过 Interop 提供的增强 Office 对象, 可以对 Word 文档进行编程操作. Range https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.range?view=word-pia 是 Word 中执行操作的一个单元, 可以理解成文档中一个选中的部分或者区域, 针对这个选中部分, 可以应用格式, 修改文字和颜色等功能. 出于各种业务的需求, 常常需要将 Word 文档或者 Word 文档的一部分变成图片. 本文对常见的 Range 转换为 Image 的方法进行了讨论和分析, 并提出一些改良的地方.
剪贴板作为中介
原理很简单, 就是将 Range.Copy()到剪贴板中, 剪贴板是 windows 进程间通信 https://blog.csdn.net/microzone/article/details/7044266 的一种方式, 然后从剪贴板中获取 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();
来源: https://www.cnblogs.com/senhtry/p/9327598.html