dotnet OpenXML 讀取 PPT 內嵌 ole 格式 Excel 表格的資訊

lindexi發表於2021-09-02

在 Office 中,可以在 PPT 裡面插入表格,插入表格有好多不同的方法,對應 OpenXML 文件儲存的更多不同的方式。本文來介紹如何讀取 PPT 內嵌 ole 格式的 xls+ 表格的方法

在 Office 的 PPT 中,插入表格可以對應多個不同的方式:

  • 通過 GraphicData 內嵌到 PPTX 頁面裡面
  • 通過嵌入檔案方式
  • 通過 SmartArt 模擬的表格,本質上就是 SmartArt 元素

其中通過嵌入檔案方式可以分為以下不同的嵌入方式:

  • 通過外嵌 Microsoft_Excel_Worksheet.xlsx 格式,此格式可以解析。這是在 Office 2019 的預設
  • 通過外嵌 oleObject1.bin 格式,此格式是 ole 格式,裡面包含 xls+ 格式
  • 通過外嵌 oleObject1.bin 格式,此格式是 ole 格式,裡面包含了 xls 格式

什麼是 xls+ 格式?其實這個名字我沒有找到權威的文件來說明。大概是在 Office 2016 的預設行為是如此,點選表格,插入 Excel 電子表格時嵌入的文件就是此格式。這個格式存放方式是 ole 格式,在此 OLE 檔案裡面,將存放 OpenXML 格式的 xlsx 格式的表格檔案,以下將詳細告訴大家此格式

在 Slide.xml 頁面裡面,存放的是在 GraphicFrame 下的內容,簡化的 OpenXML 文件如下

      <p:graphicFrame>
        <p:nvGraphicFramePr>
          <p:cNvPr id="9" name="表格 1" />
        </p:nvGraphicFramePr>
        <p:xfrm>
          <a:off x="5405438" y="3241675" />
          <a:ext cx="3438525" cy="2009775" />
        </p:xfrm>
        <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 xmlns:v="urn:schemas-microsoft-com:vml" Requires="v">
                <p:oleObj spid="_x0000_s1026" name="工作表" r:id="rId3" imgW="3438630" imgH="2009788" progId="Excel.Sheet.12">
                  <p:embed />
                </p:oleObj>
              </mc:Choice>
              <mc:Fallback>
               <!-- 忽略 -->
              </mc:Fallback>
            </mc:AlternateContent>
          </a:graphicData>
        </a:graphic>
      </p:graphicFrame>

以上邏輯核心的就是存放的嵌入的 oleObj 物件,可以在 Slide.xml.rels 檔案裡面找到如下定義內容

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="../embeddings/oleObject1.bin" />
</Relationships>

也就是說嵌入的表格是放在 embeddings 資料夾下的 oleObject1.bin 檔案,這是一個 OLE 檔案。本質上來說 OLE 和 ZIP 等壓縮格式是同等級的,是用來做儲存的,也就是說 OLE 格式本身不是特定給 Excel 表格使用的,僅僅只是用來做儲存而已。大家是否還記得 ppt 和 pptx 的差別,上古(2003)的時候,採用的是格式是 ppt 格式,此格式的儲存就是 OLE 儲存方式,也可以這樣認為,古時候的 xls 和 ppt 等都是 OLE 檔案。但是新版本的 pptx 和 xlsx 等都是 OpenXML 格式

嵌入到 PPT 的 oleObject1.bin 也就是 OLE 檔案,對應上古的格式。但是有一些不同的是,此檔案不屬於 xls 檔案格式,而是細分為兩個類別,其中一個是在 OLE 裡面存放 xls 的,另一個存放的是 xlsx 的。也就是說需要將 oleObject1.bin 展開,才可以獲取裡面的表格檔案。本文將在 OLE 裡面存放 xlsx 格式的嵌入方式稱為 xls+ 格式

先來開始從 OpenXML 文件讀取到 OLE 嵌入檔案的邏輯

和通用的 PPTX 檔案解析相同的邏輯,先讀取檔案,我的測試檔案在首頁就嵌入了表格。本文所有的程式碼和測試檔案都可以在本文末尾找到下載方式

            var file = new FileInfo("Test.pptx");

            using var presentationDocument = PresentationDocument.Open(file.FullName, false);
            var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;

接下來獲取 GraphicFrame 和裡層的資訊

            var graphicFrame = slide.CommonSlideData!.ShapeTree!.GetFirstChild<GraphicFrame>()!;
            var graphic = graphicFrame.Graphic!;
            var graphicData = graphic.GraphicData!;

如上述文件,在 GraphicData 裡面存放的是 AlternateContent 元素,此元素裡面再嵌入 OLE 檔案

            var alternateContent = graphicData.GetFirstChild<AlternateContent>()!;
            var choice = alternateContent.GetFirstChild<AlternateContentChoice>()!;
            var oleObject = choice.GetFirstChild<OleObject>()!;
            Debug.Assert(oleObject.GetFirstChild<OleObjectEmbed>() != null);

通過以上邏輯即可獲取到對應的 OleObject 物件。本文上面的例子程式碼僅僅只是用於本文的測試檔案,對於其他檔案不確定是否存在表格的,還請自行判斷空,而不是採用本文的斷言方式。本文的例子裡的程式碼為了清晰,就不新增其他分支判斷

以上程式碼拿到了 OleObject 即可獲取到對應的 oleObject1.bin 檔案。在 OpenXML SDK 裡面,不會真的將 PPTX 檔案解壓縮,原因有兩個:第一個是效能考慮,第二個是有一些內容解壓縮之後會丟失資訊(不是使用檔案存放的,只是相容zip格式而已)而導致了嘗試使用路徑讀取 oleObject1.bin 檔案是不可行的。通過 dotnet OpenXML 為什麼資源使用 Relationship 引用 部落格瞭解到,讀取方法如下

            var id = oleObject.Id!;
            var part = slide.SlidePart!.GetPartById(id!);
            Debug.Assert(part.ContentType== "application/vnd.openxmlformats-officedocument.oleObject");

使用 part.GetStream(FileMode.Open) 就可以開啟 oleObject1.bin 對應的 Stream 物件

然而這是一個 OLE 物件,為了解析此檔案,我們需要引入一個基於 MPL 協議(寬鬆,可商業,無須開源)的 Open MCDF 庫,這是一個完全由 C# 實現的讀取 OLE 格式文件的庫,在我做 VisualStudio 外掛時也用到,請看 dotnet Roslyn 通過讀取 suo 檔案獲取解決方案的啟動專案

在 csproj 上新增如下程式碼進行安裝 Open MCDF

  <PackageReference Include="OpenMcdf" Version="2.2.1.9" />

當前的 csproj 專案檔案程式碼如下

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <None Update="Test.pptx">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="dotnetCampus.OpenXmlUnitConverter" Version="1.7.1" />
    <PackageReference Include="DocumentFormat.OpenXml" Version="2.13.1" />
    <PackageReference Include="OpenMcdf" Version="2.2.1.9" />
  </ItemGroup>

</Project>

儘管在 Open MCDF 庫提供了 CompoundFile 的建構函式可以傳入 Stream 物件,但是因為在 OpenXML 的 Part 取出的 Stream 是不可隨機訪問的(為了解決 N 多的坑,在 System.IO.Packaging 限制)因此以下程式碼是不可用的

 var compoundFile = new CompoundFile(part.GetStream(FileMode.Open));

執行上面程式碼將會提示 OpenMcdf.CFException:“Cannot load a non-seekable Stream” 而失敗

為了使用 Open MCDF 庫讀取,需要先存放到本地檔案,程式碼如下

            var tempFolder = System.IO.Path.GetTempPath();

            var oleFile = System.IO.Path.Combine(tempFolder, System.IO.Path.GetRandomFileName());
            using (var fileStream = File.OpenWrite(oleFile))
            {
                using var stream = part.GetStream(FileMode.Open);
                stream.CopyTo(fileStream);
            }

開啟此 OLE 檔案程式碼如下

            var compoundFile = new CompoundFile(oleFile);

從此 OLE 檔案讀取出 xlsx 檔案的程式碼如下

            var packageStream = compoundFile.RootStorage.GetStream("Package");
            var xlsxFile = System.IO.Path.Combine(tempFolder, System.IO.Path.GetRandomFileName()+".xlsx");
            using (var fileStream = File.OpenWrite(xlsxFile))
            {
                fileStream.Write(packageStream.GetData().AsSpan());
            }

在獲取到 xlsxFile 檔案之後,即可進行 Excel 解析,讀取裡面的資訊

            using var spreadsheetDocument = SpreadsheetDocument.Open(xlsxFile,false);
            var sheets = spreadsheetDocument.WorkbookPart!.Workbook.Sheets;

更多讀取 Excel 的方法請看 C# dotnet WPF 使用 OpenXml 解析 Excel 檔案

本文不再詳細告訴大家如何讀取此 Excel 內容

本文以上的測試檔案和程式碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的原始碼,先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 976b039620120286bed59eda5363a87b592941ca

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

獲取程式碼之後,進入 Pptx 資料夾

更多請看 Office 使用 OpenXML SDK 解析文件部落格目錄

更多參考:

  • [MS-OFFDI].pdf
  • [MS-XLS].pdf
  • [MS-OI 29500].pdf

相關文章