我在写一个 WinForms 程序用来读取 Word 里面的图片显示,在解析 Word 等 Office 文档,会看到一些 ole object 元素,而有些 ole object 会有 Fallback 图片,用这些备用的图片可以显示 ole 元素

其实有很多 Office 插件公司在开发,而特殊的元素如何在其他版本打开?或者我用插件做了一个复杂的元素,在没有插件的设备如何让用户看到?在 Office 的一个做法是通过 Fallback 元素,在里面放一张图片

因为我的 Word 文档写了很多逗比的话,就不开放给大家。除了 Word 在 PPT 解析上也差不多,解析 PPT 里面的 Ole 元素,使用 Fallback 元素显示图片是本文的例子。这份文档也不能给大家,我不觉得你没事干会看本文,应该是你遇到了 Office 解析 ole 元素如何显示或 oleobj 如何转换等问题会看本文 ,也就是你其实有一份 Office 文档了

我将这个文档放在 “F:\林德熙是逗比” 文件夹,也就是你拿到我的代码也许需要更改一下代码里面的路径,才能跑起来

先安装 DocumentFormat.OpenXml 库,这是一个完全开源的官方的全平台的库

  <ItemGroup>
    <PackageReference Include="DocumentFormat.OpenXml" Version="2.10.1" />
  </ItemGroup>

是不是觉得我上面代码安装库很奇特,其实这是 SDK Style 格式的 csporj 的写法,可以瞬间安装完成一个 NuGet 库。如何使用这个格式请看 从以前的项目格式迁移到 VS2017 新项目格式

通过下面代码可以打开解析 Office 文件,本文打开的是一个 PPT 文件

            using (var doc = PresentationDocument.Open(pptxFilePath, false))

我推荐这部分可以放在后台代码,因为 PresentationDocument.Open 需要做的内容会比较多

上面如何打开 PPT 请看 C# dotnet 使用 OpenXml 解析 PPT 文件

我假定只有一个页面,因为我传入的PPT文件就只有一个页面,这个需要根据你的实际代码更改

                // 我假定你只有一个页面
                var slidePart = doc.PresentationPart.SlideParts.First();

元素是放在页面里面,也就是需要拿到页面

                // 拿到页面
                var slide = slidePart.Slide;

很多 ole 元素都放在 p:graphicframe 里面,先尝试遍历所有 GraphicFrame 请看代码

                // 找到所有也许是 ole 的元素
                foreach (var frame in slide.CommonSlideData.ShapeTree
                    .OfType<DocumentFormat.OpenXml.Presentation.GraphicFrame>())

在原文的写法是

<p:graphicframe>
    <p:nvgraphicframepr>
        <p:cnvpr id="8" name="Object 25">
        </p:cnvpr>
        <p:cnvgraphicframepr>
            <a:graphicframelocks nochangeaspect="1">
            </a:graphicframelocks>
        </p:cnvgraphicframepr>
        <p:nvpr>
            <p:extlst>
            </p:extlst>
        </p:nvpr>
    </p:nvgraphicframepr>
    <a:graphic>
        <a:graphicdata uri="http://schemas.openxmlformats.org/presentationml/2006/ole">
            <mc:alternatecontent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
                <mc:choice requires="v" xmlns:v="urn:schemas-microsoft-com:vml">
                    <p:oleobj imgh="2535890" imgw="5253016" name="文档" progid="Word.Document.12" r:id="rId3" spid="_x0000_s1026">
                        <p:embed>
                        </p:embed>
                    </p:oleobj>
                </mc:choice>
                忽略一些代码
            </mc:alternatecontent>
        </a:graphicdata>
    </a:graphic>
</p:graphicframe>

也就是 ole 元素 p:oleobj 放在 graphic 里面,不过在 OpenXML SDK 可以使用 Linq 的方式快速读取到对应的值

var oleElement = frame.Descendants<DocumentFormat.OpenXml.Presentation.OleObject>()
    .FirstOrDefault();
if (oleElement != null)
{
}

这里的 Ole Object 在 ECMA 全称 Global Element for Embedded objects and Controls 元素

然后尝试读取 oleElement 的 Fallback 是否有图片

不是所有的 ole element 都有备用的图,需要看你的文档里面是否有 mc:fallback 元素,同时这个元素是 p:pic 图片元素

在Office的图片填充用的是 p:blipFill 而这个元素需要用到依赖的元素

简单的图片是

<p:pic>  
  …  
  <p:blipFill>  
    <a:blip r:embed="rId2"/>  
    <a:stretch>  
      <a:fillRect/>  
    </a:stretch>  
  </p:blipFill>  
  …  
</p:pic>

核心在于 r:embed 的值,这个值可以从 xml.rel 里面读取,但是这里的读取逻辑很复杂。不过 OpenXML SDK 已经封装了

那么如何从拿到 OleObject 返回备用图片,先拿到对应的页面,所有资源放在页面的 SlidePart 元素

        private static (bool isSuccess, FileInfo file) TryGetFallbackImage(OleObject oleElement)
        {
            var slide = oleElement.Ancestors<Slide>().First();

            var slidePart = slide.SlidePart;

            // 忽略代码
        }

在 OpenXML 用的类是继承 XmlDocument 也就是可以通过 oleElement 向上找到 p:graphicframe 元素

            var frame = oleElement.Ancestors<GraphicFrame>().First();

            var frameGraphic = frame.Graphic.GraphicData;

先补充 mc:fallback 的文档

<a:graphicdata uri="http://schemas.openxmlformats.org/presentationml/2006/ole">
    <mc:alternatecontent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
        <mc:choice requires="v" xmlns:v="urn:schemas-microsoft-com:vml">
            <p:oleobj imgh="2535890" imgw="5253016" name="文档" progid="Word.Document.12" r:id="rId3" spid="_x0000_s1026">
                <p:embed>
                </p:embed>
            </p:oleobj>
        </mc:choice>
        <mc:fallback>
            <p:oleobj imgh="2535890" imgw="5253016" name="文档" progid="Word.Document.12" r:id="rId3">
                <p:embed>
                </p:embed>
                <p:pic>
                    <p:nvpicpr>
                        <p:cnvpr id="0" name="">
                        </p:cnvpr>
                        <p:cnvpicpr>
                            <a:piclocks nochangearrowheads="1" nochangeaspect="1">
                            </a:piclocks>
                        </p:cnvpicpr>
                        <p:nvpr>
                        </p:nvpr>
                    </p:nvpicpr>
                    <p:blipfill>
                        <a:blip r:embed="rId4">
                            <a:extlst>
                                <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
                                    <a14:uselocaldpi val="0" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main">
                                    </a14:uselocaldpi>
                                </a:ext>
                            </a:extlst>
                        </a:blip>
                        <a:srcrect>
                        </a:srcrect>
                        <a:stretch>
                            <a:fillrect>
                            </a:fillrect>
                        </a:stretch>
                    </p:blipfill>
                    </p:sppr>
                </p:pic>
            </p:oleobj>
        </mc:fallback>
    </mc:alternatecontent>
</a:graphicdata>

从上面文档代码可以看到 mc:fallback 可以通过 frameGraphic.Descendants<DocumentFormat.OpenXml.AlternateContentFallback>().FirstOrDefault() 拿到

而对应的图片可以使用下面代码拿到

            var fallback = frameGraphic.Descendants<DocumentFormat.OpenXml.AlternateContentFallback>().FirstOrDefault();
            if (fallback == null)
            {
                return (false, null);
            }

            var picture = fallback.Descendants<DocumentFormat.OpenXml.Presentation.Picture>().FirstOrDefault();

            if (picture == null)
            {
                return (false, null);
            }

重要的代码是如何拿到 picture 的 a:blip r:embed="rId4" 的 rId4 的图片

在 OpenXML SDK 定义好了 BlipFill 可以通过下面代码拿到 rId 的值

            var embed = picture.BlipFill.Blip.Embed.Value;

而拿到 embed 就可以拿到对应的 Stream 可以写入文件

            var part = slidePart.GetPartById(embed);

            if (part is ImagePart imagePart)
            {
                if (imagePart.ContentType == "image/x-wmf" || imagePart.ContentType == "image/x-emf")
                {
                    var stream = part.GetStream(FileMode.Open, FileAccess.Read);
                    var fileName = Path.GetFileName(imagePart.Uri.OriginalString);
                    var file = Path.Combine(@"F:\林德熙是逗比", fileName);
                    File.WriteAllBytes(file, ReadAllBytes(stream));

                    return (true, new FileInfo(file));
                }
            }

上面代码写入文件是 “F:\林德熙是逗比” 小伙伴需要按照你的需求更改,上面代码也没有释放资源

这里的 ReadAllBytes 通过将 Stream 转 byte[] 方法

        private static byte[] ReadAllBytes(Stream stream)
        {
            using (var memoryStream = new MemoryStream())
            {
                stream.CopyTo(memoryStream);
                return memoryStream.ToArray();
            }
        }

在 OpenbXML SDK 可以方便拿到资源,通过 var part = slidePart.GetPartById(embed) 方法,此时返回的是 part 可以用 GetStream 返回压缩包里面的资源

其实在 WinForms 可以通过更简单的代码创建图片

System.Drawing.Image img = System.Drawing.Image.FromStream(imagePart.GetStream());

这样就能完成在 Office 文件解析 ole 元素,但是只要 ole 元素没有写 Fallback 本文方法也没有用

如果我只有 ole 元素,我能否显示,有大神写了 The DotNet Heaven: Read OLE Object type image field in C#.net

本文代码放在 github 欢迎小伙伴访问,如果无法下载源代码,请到 gitee 下载

How to parse embedded file(OLE obejct) in pptx/docx · Issue #644 · OfficeDev/Open-XML-SDK


本文会经常更新,请阅读原文: https://dotnet-campus.github.io//post/Office-OpenXml-SDK-%E4%BD%BF%E7%94%A8-Fallback-%E5%9B%BE%E7%89%87%E6%98%BE%E7%A4%BA-Ole-%E5%85%83%E7%B4%A0.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 lindexi (包含链接: https://dotnet-campus.github.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系