天天看點

dotnet OpenXML SDK 文本占位符解析

在使用 OpenXML SDK 解析 PPT 文檔的文本占位符的時候,需要對 PPT 的格式有一定的了解,盡管整個 OpenXML SDK 包括文檔等都很詳細。但是有一些細節文檔上雖然有寫,但是沒有強調一下,就被我忽略了

什麼是文本占位符,其實這是在 PPT 添加的概念,在 PPT 裡面使用者可以編輯模版檔案,在這裡定義某個占位符文本的樣式和坐标等

如何制作占位符請看 PPT占位符,居然這麼好用! - 知乎

想要解析占位符還需要先學會如何使用占位符才好了解占位符是如何做的

在 OpenXML 裡面文本是形狀,也就是 DocumentFormat.OpenXml.Presentation.Shape 元素,可以使用下面代碼擷取頁面的形狀

using (var presentationDocument = DocumentFormat.OpenXml.Packaging.PresentationDocument.Open("測試.pptx", false))
            {
                var presentationPart = presentationDocument.PresentationPart;
                var presentation = presentationPart.Presentation;

                // 先擷取頁面
                var slideIdList = presentation.SlideIdList;

                foreach (var slideId in slideIdList.ChildElements.OfType<SlideId>())
                {
                    // 擷取頁面内容
                    SlidePart slidePart = (SlidePart) presentationPart.GetPartById(slideId.RelationshipId);

                    var slide = slidePart.Slide;

                    foreach (var shape in
                        slidePart.Slide
                            .Descendants<DocumentFormat.OpenXml.Presentation.Shape>())
                    {
                       
                    }
                }
            }      

在 PPT 裡面是使用 p:ph 判斷一個形狀是占位符,下面是一個占位符的形狀

<p:sp>
    <p:nvsppr>
        <p:cNvPr id="2" name="标題 1">
        </p:cNvPr>
        <p:cNvSpPr>
            <a:splocks nogrp="1">
            </a:splocks>
        </p:cNvSpPr>
        <p:nvpr>
            <p:ph type="ctrTitle">
            </p:ph>
        </p:nvpr>
    </p:nvsppr>
    <p:sppr>
    </p:sppr>
    <p:txbody>
        <a:bodypr>
        </a:bodypr>
        <a:p>
            <a:r>
                <a:rpr altlang="en-US" lang="zh-CN">
                </a:rpr>
                <a:t>
                    PPT 解析
                </a:t>
            </a:r>
            <a:endpararpr altlang="en-US" lang="zh-CN">
            </a:endpararpr>
        </a:p>
    </p:txbody>
</p:sp>      

在 slide.xml 裡面的元素,通過設定 nvsppr->nvpr->ph 設定這個元素使用占位符,需要繼承模版的占位符樣式和坐标等值

從 Shape 裡面拿到占位符可以使用下面代碼

// <p:nvSpPr>占位符的樣式
NonVisualShapeProperties nonVisualShapeProperties = shape?.NonVisualShapeProperties;
// cNvSpPr
var nonVisualShapeDrawingProperties = nonVisualShapeProperties?.NonVisualShapeDrawingProperties;
// nvpr
var applicationNonVisualDrawingProperties = nonVisualShapeProperties?.ApplicationNonVisualDrawingProperties;

var placeholderShape = applicationNonVisualDrawingProperties?.PlaceholderShape;      

可以在 placeholderShape 裡面找到兩個主要的屬性,一個是 Index 一個是 Type 屬性,這兩個屬性是什麼意思?從屬性的注釋可以看到寫的很複雜,大概的做法就是占位符需要去找到模版裡面相同的 Index 或相同的 Type 的占位符元素,擷取這個元素的樣式和坐标等

如果有仔細閱讀上面文檔就可以知道,如果使用者在模版裡面定義了占位符,那麼僅僅表示頁面裡面的對應元素的預設樣式,而元素本文可以覆寫此樣式。也就是元素的最終樣式是先嘗試擷取元素本文的樣式,如果元素本文擷取不到樣式,那麼嘗試運作占位符元素,如果可以找到占位符元素,那麼嘗試擷取占位符元素的對應樣式

那麼如何通過 placeholderShape 找到對應的放在模版裡面的占位符元素?是否小夥伴還記得 Slide Layout 和 Slide Master 的概念,如果不知道的話,請複習一下 PPT 是如何制作的課程,這兩個概念有點繞,需要小夥伴學會制作 PPT 才比較好說

擷取 SlideLayout 和 SlideMaster 可以使用下面代碼

var layout = slidePart.SlideLayoutPart.SlideLayout;
var master = slidePart.SlideLayoutPart.SlideMasterPart.SlideMaster;      
下面這句話是錯的 先從 layout 裡面嘗試找到有沒有對應的占位符元素,如果沒有找到再從 master 裡面找

無論是 SlideLayout 還是 SlideMaster 都在 CommonSlideData 的 ShapeTree 存放占位符元素。是以尋找占位符方法是從 CommonSlideData 的 ShapeTree 尋找是否有對應的元素,那麼什麼是對應的元素,如果頁面元素設定了 Type 那麼要求 ShapeTree 的元素的占位符屬性有完全相同的 Type 屬性,如果頁面元素設定了 Index 那麼要求 ShapeTree 的有相同的 ShapeTree 屬性。如果頁面元素的 Type 是空,那麼就不對 ShapeTree 的屬性有要求,如果 Index 是空,那麼對 ShapeTree 的屬性也沒有要求

private static Shape GetPlaceholderShape(PlaceholderShape placeholder, ShapeTree tree)
        {
            return tree.Elements<Shape>().FirstOrDefault(shape =>
            {
                var placeholderShape = shape?.NonVisualShapeProperties?.ApplicationNonVisualDrawingProperties
                    ?.PlaceholderShape;
                return placeholderShape != null && Equals(placeholder, placeholderShape);
            });
        }

        /// <summary>
        /// 比較<see cref="PlaceholderShape"/>的type和id是否相同
        /// <para></para>
        /// 如果 1 的 Type 或 Index 是空,那麼跳過這個屬性的判斷
        /// <para></para>
        /// 如果這個屬性不是空,那麼一定要求 2 存在這個屬性
        /// </summary>
        /// 這個規則通過 文本占位符沒有type和id的值,擷取第一個占位符作為坐标 和 WPS 對比測試拿到
        /// 測試課件:文本占位符沒有type和id的值.pptx
        /// <param name="placeholder1"></param>
        /// <param name="placeholder2"></param>
        /// <returns></returns>
        private static bool Equals(PlaceholderShape placeholder1, PlaceholderShape placeholder2)
        {
            // 如果 placeholder1.Type 存在值,要求 2 一定存在值
            if (placeholder1.Type != null && 
                placeholder1.Type.Value != placeholder2.Type?.Value)
            {
                return false;
            }

            if (placeholder1.Index != null && placeholder1.Index.Value !=
                placeholder2.Index?.Value)
            {
                return false;
            }

            return true;
        }      

擷取的方法如下

layoutPlaceholder = GetPlaceholderShape(placeholder, layout?.CommonSlideData?.ShapeTree);
  masterPlaceholder = GetPlaceholderShape(placeholder, master?.CommonSlideData?.ShapeTree);      
下面這句話是不對的 此時的樣式擷取順序就是先從元素擷取,如果元素擷取不到,就從 layoutPlaceholder 擷取,如果擷取不到從 masterPlaceholder 擷取

注釋裡面的 文本占位符沒有type和id的值.pptx 我就不放出來了,有需要的小夥伴發郵件給我

更多的 OpenXML 相關部落格,還請自行百度 OpenXML 林德熙 就能找到我的部落格了

更正一下

小夥伴可以看到我标記了文章一些說法是不對的。原因是 ECMA 376 文檔裡面其實隻包含了 Placeholder 的定義,而沒有包含他的實作方式。整個 ECMA 關于 Placeholder 僅有 274 個引用。是以我上面這裡的說法完全隻是因為沒有實踐而依靠不靠譜的部落格找到的方法

以下為我通過 Office 2013 和 Office 2016 和 WPS 11.3.0.8742 版本測試拿到的規則

  • 占位符元素需要同時在 SlideLayout 和 SlideMaster 裡面查找
  • 占位符元素的屬性優先級是 Slide 裡元素本身,然後是 SlideLayout 占位符元素最後是 SlideMaster 占位符元素
  • 假定嘗試擷取元素的平移屬性,此時在元素本身沒有找到,就需要從 SlideLayout 占位符元素(如果存在)裡嘗試擷取平移屬性
  • 假定從 SlideLayout 占位符元素擷取不到平移屬性,那麼嘗試從 SlideMaster 占位符元素擷取平移屬性
  • 占位符元素如果設定了 Id 的值,那麼标準文檔裡面這個 Id 在 SlideLayout 和 SlideMaster 僅能找到一個占位符元素,不會存在兩個
  • 對 WPS 非标準文檔,可能存在兩個相同 Id 元素,此時使用 xml 的第一個元素
  • 占位符元素如果設定了 Id 的值,在 SlideMaster 裡面沒有找到對應的 Id 的占位符元素,那麼嘗試通過占位符元素的 Type 找到對應的 SlideMaster 的元素
  • 如果占位符元素沒有設定 Type 的值,那麼預設值是 Body 的值
  • 如果在 SlideMaster 裡面存在多個 Body 元素,那麼标準文檔裡面将會設定每個 Body 元素都有 Id 的值
  • 對 WPS 非标準文檔,如果定義多個 Body 元素,且沒有給每個 Body 元素設定 Id 的值,那麼使用 xml 的第一個 Body 元素
  • 對 WPS 非标準文檔,如果定義多個 Body 元素,其中有一個 Body 元素沒有 Id 的值,且使用 Id 查找不到對應占位符元素,那麼使用第一個沒有 Id 的 Body 元素

大概邏輯如下,下面代碼僅使用 SlideMaster 的占位符元素,原因是 SlideLayout 沒有遇到此非标準文檔,而我也不想去改文檔代碼測試

/// <summary>
        /// 對 SlideMaster 的擷取占位符的規則是假設 PlaceholderShape 存在 Id 的值,那麼在 SlideMaster 所有元素嘗試找到對應的 Id 的值的元素,如果能找到那麼這個元素就是占位符元素。如果不存在 Id 或找不到對應的元素,那麼進行 Type 的查找,如果傳入的 PlaceholderType 沒有設定值,那麼将使用預設的 PlaceholderValues.Body 的值
        /// </summary>
        /// <returns></returns>
        private static Shape GetMasterPlaceholderByPlaceHolderType(PlaceholderShape placeholder, SlideMaster master)
        {
            EnumValue<PlaceholderValues> placeholderType = placeholder.Type;
            const PlaceholderValues defaultPlaceholderValue = PlaceholderValues.Body;

            var type = placeholderType?.Value ?? defaultPlaceholderValue;
            var id = placeholder.Index?.Value;

            var elementList = master?.CommonSlideData?.ShapeTree?.Elements<Shape>();
            if (elementList == null)
            {
                return null;
            }

            Shape typeMatchShape = null;

            foreach (var shape in elementList)
            {
                var placeholderShape = shape?.NonVisualShapeProperties?.ApplicationNonVisualDrawingProperties
                    ?.PlaceholderShape;

                if (placeholderShape == null)
                {
                    continue;
                }

                if (id != null)
                {
                    // 優先找到 id 相同的占位符
                    // 如果 id 相同的找不到,那麼找 Type 相同的
                    if (placeholderShape?.Index?.Value == id.Value)
                    {
                        return shape;
                    }
                }

                // 以下邏輯不全對,注意在有兩個Body元素,此時 id != null 且這兩個 Body 元素
                // 第一個元素存在 Id 且和 placeholder.Index 不相等
                // 第二個元素不存在 Id 的值 
                // 那麼此時應該選用第二個元素,不應該選擇第一個元素
                // 但是下面代碼選用的是第一個元素
                if (placeholderShape.Type?.Value == type)
                {
                    // 基本隻有一個 Type 相等,如果有多個,那麼這個課件不是标準的
                    Debug.Assert(typeMatchShape == null);
                    typeMatchShape = shape;
                }
            }

            return typeMatchShape;
        }      

上面代碼的邏輯不全對,我寫在注釋裡面

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

我搭建了自己的部落格 https://blog.lindexi.com/ 歡迎大家通路,裡面有很多新的部落格