dotnet OpenXML 讀取 PPT 形狀邊框定義在 Style 的顏色畫刷


本文來和大家聊聊在 PPT 形狀使用了 Style 樣式的顏色畫刷讀取方法

在開始之前,期望大家已了解如何在 dotnet 應用里面讀取 PPT 文件,如果還不了解讀取方法,請參閱 C# dotnet 使用 OpenXml 解析 PPT 文件

本文核心是來和大家聊聊 ECMA 376 文檔里面,第 20.1.4.2.19 章的 lnRef (Line Reference) 內容里面沒有提到的知識

在 Office 的 PowerPoint 添加默認的形狀,在沒有更改形狀的填充和輪廓,形狀使用的是默認的樣式,如以下的默認矩形定義

<p:sp>
  <p:nvSpPr>
    <p:cNvPr id="6" name="矩形 1" />
    <p:cNvSpPr />
    <p:nvPr />
  </p:nvSpPr>
  <p:spPr>
    <a:xfrm>
      <a:off x="3640346" y="1595887" />
      <a:ext cx="3804249" cy="3071004" />
    </a:xfrm>
    <a:prstGeom prst="rect">
      <a:avLst />
    </a:prstGeom>
    <a:ln w="76200" />
  </p:spPr>
  <p:style>
    <a:lnRef idx="2">
      <a:schemeClr val="accent1">
        <a:shade val="50000" />
      </a:schemeClr>
    </a:lnRef>
    <a:fillRef idx="1">
      <a:schemeClr val="accent1" />
    </a:fillRef>
    <a:effectRef idx="0">
      <a:schemeClr val="accent1" />
    </a:effectRef>
    <a:fontRef idx="minor">
      <a:schemeClr val="lt1" />
    </a:fontRef>
  </p:style>
  <p:txBody/>
</p:sp>

在 OpenXML 里面,通過 a:ln 表示 Outline 輪廓,也就是咱 WPF 形狀元素的邊框。包括定義了邊框粗細和顏色畫刷等

但是從上面文檔內容可以看到,只是定義了邊框的粗細,沒有定義顏色。這就需要從 <p:style> 樣式里面讀取線條的樣式。從 <a:lnRef idx="2"> 可以讀取到采用的是序號為 2 的線條樣式,這里有一個細節是給定的序號也許會超過定義,解決方法請看 dotnet OpenXML 讀取形狀輪廓線條樣式序號超過主題樣式列表數

接着讀取 <a:schemeClr val="accent1"> 的內容,用來覆蓋作為實際的顏色

下面我將給大家演示如何在 WPF 中讀取 PPT 的形狀 Style 邊框顏色和在界面里面將此顯示出來

先加上基礎的庫引用,以下代碼寫在 csproj 上,可在本文末尾找到全部源代碼

  <ItemGroup>
    <PackageReference Include="dotnetCampus.OpenXmlUnitConverter" Version="1.4.1"/>
    <PackageReference Include="DocumentFormat.OpenXml" Version="2.13.1" />
  </ItemGroup>

接着讀取包含上文的形狀的 PPT 文檔

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

            using var presentationDocument = PresentationDocument.Open(file.FullName, false);

獲取頁面里面的形狀,如以下代碼

            var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;
            var shape = slide.CommonSlideData!.ShapeTree!.GetFirstChild<Shape>()!;

先從 ShapeProperties 里面獲取形狀的邊框粗細,如下面代碼

            ShapeProperties shapeProperties = shape.ShapeProperties!;
            // 雖然這個形狀有輪廓,但是定義是 `<a:ln w="76200" />` 只有寬度,沒有顏色
            Outline outline = shapeProperties.GetFirstChild<Outline>()!;
            Debug.Assert(outline.GetFirstChild<SchemeColor>() is null);
            var outlineWidth = new Emu(outline.Width!);

以上代碼拿到的 outlineWidth 就是形狀的邊框粗細。此形狀有輪廓,但是定義是 <a:ln w="76200" /> 只有寬度,沒有顏色。顏色需要在 Style 里面讀取。在 PPTX 文件里面的定義如下

    <a:lnRef idx="2">
      <a:schemeClr val="accent1">
        <a:shade val="50000" />
      </a:schemeClr>
    </a:lnRef>

獲取方法是先讀取形狀的樣式,接着讀取線條引用,請看代碼

            // 實際的顏色應該從 `<a:lnRef idx="2">` 拿到
            var shapeStyle = shape.ShapeStyle!;
            var lineReference = shapeStyle.LineReference!;

拿到 LineReference 就可以讀取里層的顏色,如下面代碼

            // 讀取里層的顏色
            var schemeColor = lineReference.GetFirstChild<SchemeColor>()!;

此顏色是 SchemeColor 顏色,按照 dotnet OpenXML 如何獲取 schemeClr 顏色 文檔的方法進行讀取,讀取時用到的輔助方法本文就不列出,還請參閱以上引用博客。當然,本文所有源代碼都可以獲取到,還請不用擔心細節

以下是將 SchemeColor 轉換為 System.Windows.Media.Color 的方法

            var colorMap = slide.GetColorMap()!;
            var colorScheme = slide.GetColorScheme()!;
            var value = schemeColor.Val!.Value; // accent1
            value = ColorHelper.SchemeColorMap(value, colorMap);
            var actualColor = ColorHelper.FindSchemeColor(value, colorScheme)!; // <a:srgbClr val="5B9BD5"/>
            RgbColorModelHex rgbColorModelHex = actualColor.RgbColorModelHex!;
            var color = (Color) ColorConverter.ConvertFromString($"#{rgbColorModelHex.Val!.Value}");

接下來按照其他形狀的解析方法,讀取坐標和寬度高度,在界面顯示出來

            // 獲取坐標
            var offset = shapeProperties.Transform2D!.Offset!;
            var x = new Emu(offset.X!);
            var y = new Emu(offset.Y!);
            var extents = shapeProperties.Transform2D.Extents!;
            var width = new Emu(extents.Cx!);
            var height = new Emu(extents.Cy!);

            // 創建元素
            var rectangle = new Rectangle()
            {
                Margin = new Thickness(x.ToPixel().Value, y.ToPixel().Value, 0, 0),
                Width = width.ToPixel().Value,
                Height = height.ToPixel().Value,
                StrokeThickness = outlineWidth.ToPixel().Value,
                Stroke = new SolidColorBrush(color)
            };

            Canvas.Children.Add(rectangle);

以上代碼的 Canvas 是在 XAML 定義的,代碼如下

  <Grid>
    <Canvas x:Name="Canvas">
    </Canvas>
  </Grid>

運行之后的效果如下

可以看到顏色其實有些差距,原因是以上使用的 SchemeColor 沒有加上顏色特效,在 PPTX 文件定義的顏色代碼如下

      <a:schemeClr val="accent1">
        <a:shade val="50000" />
      </a:schemeClr>

通過 dotnet OpenXML 顏色變換 文檔可以了解到 Shade 是讓顏色變暗,使用如下代碼加上特效

            var shade = schemeColor.GetFirstChild<Shade>()!;// 讓顏色變暗
            color = ColorTransform.HandleShade(color, shade);

此時的效果如下

本文以上所有測試文件和代碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼

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

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

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

獲取代碼之后,進入 Pptx 文件夾

雖然可以看到在 WPF 上的形狀的邊框顏色和在 PPT 上的形狀顏色是相同的,然而以上邏輯卻有漏洞在於以上是重新被定義了顏色。其實在 OpenXML 里面,按照的尋找屬性的規則和 WPF 的資源是相同的邏輯,按照最近原則讀取。也就是說讀取順序如下

  • 形狀的 a:ln 定義的顏色
  • 形狀的樣式的 a:lnRef 引用的主題的顏色
  • 形狀繼承的樣式

以上的測試文檔是屬於在形狀的 a:ln 沒有定義的顏色,而在形狀的樣式的 a:lnRef 里面定義的顏色,而且形狀引用樣式里面使用的是 <a:schemeClr val="phClr" /> 占位符顏色

如果在形狀的 a:ln 和形狀的樣式的 a:lnRef 沒有定義的顏色,只有在形狀的樣式的 a:lnRef 引用的主題的顏色,那么以上邏輯是不符合 OpenXML 定義的。或者說在形狀的 a:ln 沒有定義的顏色,而在形狀的樣式的 a:lnRef 里面有定義顏色,但是形狀的樣式的 a:lnRef 引用的主題的顏色不是 phClr (PlaceholderColor, a color used in theme definitions which means "use the color of the style")的顏色

如以下的文檔內容

Slide1.xml:

<p:sp>
  <p:style>
    <a:lnRef idx="2">
      <a:schemeClr val="accent1" />
    </a:lnRef>
  </p:style>
</p:sp>

Theme1:

      <a:lnStyleLst>
        <a:ln w="6350" cap="flat" cmpd="sng" algn="ctr">
        </a:ln>
        <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr">
          <a:solidFill>
            <a:srgbClr val="999999" />
          </a:solidFill>
          <a:prstDash val="solid" />
          <a:miter lim="800000" />
        </a:ln>
        <a:ln w="19050" cap="flat" cmpd="sng" algn="ctr">
        </a:ln>
      </a:lnStyleLst>

通過 <a:lnRef idx="2"> 可以了解到應該讀取的是 LineStyleList 的第二項(從1開始)的顏色

和第一份文檔不同的是,以上文檔定義的是 <a:srgbClr val="999999" /> 而不是 <a:schemeClr val="phClr" /> 顏色。也就是說在 Slide1.xml 定義的 <a:schemeClr val="accent1" /> 需要被忽略

變更之后的邏輯如下,先讀取引用,參閱 dotnet OpenXML 讀取形狀輪廓線條樣式序號超過主題樣式列表數 文檔的寫法

            var lineStyle = lineReference.Index!.Value;// idx="2"
            lineStyle--;
            // 獲取主題
            var slidePart = slide.SlidePart!;
            var themeOverride = slidePart.ThemeOverridePart?.ThemeOverride
                                ?? slidePart.SlideLayoutPart!.ThemeOverridePart?.ThemeOverride;
            FormatScheme? formatScheme = themeOverride?.FormatScheme;
            if (formatScheme is null)
            {
                formatScheme = slidePart.SlideLayoutPart!.SlideMasterPart!.ThemePart!.Theme!.ThemeElements!.FormatScheme;
            }
            var lineStyleList = formatScheme!.LineStyleList;
            var outlineList = lineStyleList!.Elements<Outline>().ToList(); // 其實,別用 ToList 的好,這里只是簡化邏輯
            Outline themeOutline;
            if (lineStyle > outlineList.Count)
            {
                themeOutline = outlineList[^1];
            }
            else
            {
                themeOutline = outlineList[(int)lineStyle];
            }

此時讀取到的 themeOutline 就是如下的 OpenXML 文檔內容

        <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr">
          <a:solidFill>
            <a:srgbClr val="999999" />
          </a:solidFill>
          <a:prstDash val="solid" />
          <a:miter lim="800000" />
        </a:ln>

先獲取是否有定義 solidFill 的內容,如果有,那么繼續獲取里層顏色

var solidFill = themeOutline.GetFirstChild<SolidFill>()!;

在以上的文檔里面,是存在 SolidFill 內容的,因此上面代碼就不判斷空了

獲取里層的顏色,如果是 srgbClr (對應 OpenXML 的 RgbColorModelHex 類型)的話,那么計算顏色即可

            var solidFill = themeOutline.GetFirstChild<SolidFill>()!;
            var colorModelHex = solidFill.GetFirstChild<RgbColorModelHex>();
            if (colorModelHex is not null)
            {
            	// <a:srgbClr val="999999" />
                color = (Color)ColorConverter.ConvertFromString($"#{colorModelHex.Val!.Value}");
            }

而如果是讀取到 SchemeColor 而且是 PhColor 方式的顏色,那么依然按照上文的方式讀取形狀樣式里面的 LineReference 的顏色

    <a:lnRef idx="2">
      <a:schemeClr val="accent1">
        <a:shade val="50000" />
      </a:schemeClr>
    </a:lnRef>

讀取的邏輯如下

            var solidFill = themeOutline.GetFirstChild<SolidFill>()!;
            var colorModelHex = solidFill.GetFirstChild<RgbColorModelHex>();
            if (colorModelHex is not null)
            {
            }
            else
            {
                var themeSchemeColor = solidFill.GetFirstChild<SchemeColor>()!;
                if (themeSchemeColor.Val!.Value == SchemeColorValues.PhColor)
                {
                    // 讀取里層的顏色
                    var schemeColor = lineReference.GetFirstChild<SchemeColor>()!;
                    // 讀取 SchemeColor 方法請參閱如下文檔
                    // [dotnet OpenXML 如何獲取 schemeClr 顏色](https://blog.lindexi.com/post/dotnet-OpenXML-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-schemeClr-%E9%A2%9C%E8%89%B2.html )
                    var colorMap = slide.GetColorMap()!;
                    var colorScheme = slide.GetColorScheme()!;
                    var value = schemeColor.Val!.Value; // accent1
                    value = ColorHelper.SchemeColorMap(value, colorMap);
                    var actualColor = ColorHelper.FindSchemeColor(value, colorScheme)!; // <a:srgbClr val="5B9BD5"/>
                    RgbColorModelHex rgbColorModelHex = actualColor.RgbColorModelHex!;
                    color = (Color)ColorConverter.ConvertFromString($"#{rgbColorModelHex.Val!.Value}");
                    // 根據 [dotnet OpenXML 顏色變換](https://blog.lindexi.com/post/dotnet-OpenXML-%E9%A2%9C%E8%89%B2%E5%8F%98%E6%8D%A2.html ) 進行修改顏色
                    var shade = schemeColor.GetFirstChild<Shade>()!;// 讓顏色變暗
                    color = ColorTransform.HandleShade(color, shade);
                }
            }

此時更換文檔,執行的界面如下

本文以上更新的測試文件和代碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 868ad6c1a39853764167053e319b68a6db0a2c6b

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

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

獲取代碼之后,進入 Pptx 文件夾

更多的特殊邏輯:

如果在 Theme 里面定義的 LineStyleList 里面定義的輪廓沒有設置顏色,如下面的 OpenXML 文檔

      <a:lnStyleLst>
        <a:ln w="6350" cap="flat" cmpd="sng" algn="ctr">
          <a:solidFill>
            <a:schemeClr val="phClr" />
          </a:solidFill>
          <a:prstDash val="solid" />
          <a:miter lim="800000" />
        </a:ln>
        <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr">
          <a:prstDash val="solid" />
          <a:miter lim="800000" />
        </a:ln>
        <a:ln w="19050" cap="flat" cmpd="sng" algn="ctr">
          <a:solidFill>
            <a:schemeClr val="phClr" />
          </a:solidFill>
          <a:prstDash val="solid" />
          <a:miter lim="800000" />
        </a:ln>
      </a:lnStyleLst>

在 Slide1.xml 里面的形狀定義和上文的相同,引用了第二項的主題,如下面的 OpenXML 文檔

    <a:lnRef idx="2">
      <a:schemeClr val="accent1">
        <a:shade val="50000" />
      </a:schemeClr>
    </a:lnRef>

此時在 PPT 的運行效果就是沒有邊框,也就是說在 a:lnRef 定義的 <a:schemeClr val="accent1"> 顏色僅僅只是用來作為 PhColor 的替換

更多請看 Office 使用 OpenXML SDK 解析文檔博客目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM