zoukankan      html  css  js  c++  java
  • dotnet OpenXML 转换 PathFillModeValues 为颜色特效

    在 OpenXml 预设形状,有一些形状设置了 PathFillModeValues 枚举,此枚举提供了亮暗的蒙层特效。具体的特效是让形状选择一个画刷,在画刷上加上特效。如立体几何 Cube 形状,在 Cube 不同的面有不同的颜色,颜色的亮度不同

    接下来通过 OpenXML SDK 实现读取 PPTX 文件,解析 Cube 预设形状,在界面绘制,让填充和 PowerPoint 的相同。如下图,下图没有绘制线条,因此看起来和 PowerPoint 显示的有一点不同

    通过 ECMA 376 文档可以了解到 Cube 由 4 个 Path 组成,公式代码如下

        <pathLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
          <path stroke="false" extrusionOk="false">
            <moveTo>
              <pt x="l" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="x4" y="y1" />
            </lnTo>
            <lnTo>
              <pt x="x4" y="b" />
            </lnTo>
            <lnTo>
              <pt x="l" y="b" />
            </lnTo>
            <close />
          </path>
          <path stroke="false" fill="darkenLess" extrusionOk="false">
            <moveTo>
              <pt x="x4" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="r" y="t" />
            </lnTo>
            <lnTo>
              <pt x="r" y="y4" />
            </lnTo>
            <lnTo>
              <pt x="x4" y="b" />
            </lnTo>
            <close />
          </path>
          <path stroke="false" fill="lightenLess" extrusionOk="false">
            <moveTo>
              <pt x="l" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="y1" y="t" />
            </lnTo>
            <lnTo>
              <pt x="r" y="t" />
            </lnTo>
            <lnTo>
              <pt x="x4" y="y1" />
            </lnTo>
            <close />
          </path>
          <path fill="none" extrusionOk="false">
            <moveTo>
              <pt x="l" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="y1" y="t" />
            </lnTo>
            <lnTo>
              <pt x="r" y="t" />
            </lnTo>
            <lnTo>
              <pt x="r" y="y4" />
            </lnTo>
            <lnTo>
              <pt x="x4" y="b" />
            </lnTo>
            <lnTo>
              <pt x="l" y="b" />
            </lnTo>
            <close />
            <moveTo>
              <pt x="l" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="x4" y="y1" />
            </lnTo>
            <lnTo>
              <pt x="r" y="t" />
            </lnTo>
            <moveTo>
              <pt x="x4" y="y1" />
            </moveTo>
            <lnTo>
              <pt x="x4" y="b" />
            </lnTo>
          </path>
        </pathLst>
    

    可以看到在 Path 元素上,有 Fill 属性,分别设置了 darkenLess 和 lightenLess 的值,对应到 OpenXML SDK 的 PathFillModeValues 类型

        public enum PathFillModeValues
        {
            [EnumString("none")]
            None,
    
            [EnumString("norm")]
            Norm,
    
            [EnumString("lighten")]
            Lighten,
    
            [EnumString("lightenLess")]
            LightenLess,
    
            [EnumString("darken")]
            Darken,
    
            [EnumString("darkenLess")]
            DarkenLess
        }
    

    在 WPF 里面绘制的逻辑是先获取所有的 Path 内容,接着将所有的 Path 绘制作为底色。底色绘制采用填充,接着按照 PathFillModeValues 的值,再次画上 Geometry 覆盖底色

    通过 WPF 多媒体提供的默认的颜色混合功能,通过颜色的 Alpha 通道让覆盖底色的部分修改亮度

    新建一个空的 WPF 应用,在应用里面放入一个创建了默认的 Cube 形状的 PPTX 文件,在窗口 Loaded 事件里面读取这份测试的文件

            public MainWindow()
            {
                InitializeComponent();
    
                Loaded += MainWindow_Loaded;
            }
    
            private void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                OpenPptxFile(new FileInfo("Test.pptx"));
            }
    

    以上的 Test.pptx 可以通过本文末尾拿到下载地址

    在开始之前,期望大家已了解如何在 dotnet 应用里面读取 PPT 文件,如果还不了解读取方法,请参阅 C# dotnet 使用 OpenXml 解析 PPT 文件

    在 OpenPptxFile 执行具体的逻辑,读取文件,获取到页面,从页面读取出形状

            private void OpenPptxFile(FileInfo file)
            {
                using var presentationDocument = PresentationDocument.Open(file.FullName, false);
                var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;
                // 这是对测试文件有要求的,要求一定传入的是立方体。请在具体的项目代码里面,替换为你需要的逻辑
                var shape = slide.Descendants<Shape>().First();
                var shapeProperties = shape.ShapeProperties;
                var presetGeometry = shapeProperties!.GetFirstChild<PresetGeometry>();
            }
    

    上面的代码只是作为测试使用,因此要求测试文件一定传入的是立方体。如果你想在自己的项目使用本文的方法,还请自行处理细节。在我的其他博客里面包含了详细的各个细节处理的逻辑,为了让本文清晰,这里就不加上太多细节逻辑

    如获取元素的尺寸的代码,这里固定默认的大小。以下代码的 EmuSize 和 Emu 都是采用 OpenXML 单位库,详细请看 Office Open XML 的测量单位

                var elementSize = new EmuSize(new Emu(1216152), new Emu(1216152));
    

    通过传入的预设形状和元素的大小,可以计算出几何信息。几何信息包含 Path 定义的内容,使用如下定义

        /// <summary>
        /// 对应PPT的Shape Path
        /// </summary>
        public readonly struct ShapePath
        {
            /// <summary>
            /// 创建PPT的Geometry Path
            /// </summary>
            /// <param name="path">OpenXml Path字符串</param>
            /// <param name="fillMode">OpenXml的Path Fill Mode  </param>
            /// <param name="isStroke">是否有轮廓</param>
            /// <param name="isExtrusionOk">指定使用 3D 拉伸可能在此路径</param>
            /// <param name="eumWidth">指定的宽度或在路径坐标系统中应在使用的最大的 x 坐标</param>
            /// <param name="eumHeight">指定框架的高度或在路径坐标系统中应在使用的最大的 y 坐标</param>
            public ShapePath(string path, PathFillModeValues fillMode = PathFillModeValues.Norm, bool isStroke = true, bool isExtrusionOk = false, double eumWidth = 0, double eumHeight = 0)
            {
                Path = path;
                IsStroke = isStroke;
                FillMode = fillMode;
                IsFilled = fillMode is not PathFillModeValues.None;
                IsExtrusionOk = isExtrusionOk;
                Width = new Emu(eumWidth);
                Height = new Emu(eumHeight);
            }
    
            /// <summary>
            /// 创建PPT的Geometry Path
            /// </summary>
            /// <param name="path">OpenXml Path字符串</param>
            /// <param name="eumWidth">指定的宽度或在路径坐标系统中应在使用的最大的 x 坐标</param>
            /// <param name="eumHeight">指定框架的高度或在路径坐标系统中应在使用的最大的 y 坐标</param>
            public ShapePath(string path, double eumWidth, double eumHeight) : this(path, PathFillModeValues.Norm, eumWidth: eumWidth, eumHeight: eumHeight)
            {
    
            }
    
            /// <summary>
            /// 是否填充
            /// </summary>
            public bool IsFilled { get; }
    
            /// <summary>
            /// OpenXml 的 Path Stroke, 默认true
            /// </summary>
            public bool IsStroke { get; }
    
            /// <summary>
            /// OpenXml的Path Fill Mode  
            /// </summary>
            public PathFillModeValues FillMode { get; }
    
            /// <summary>
            ///OpenXml  Path字符串
            /// </summary>
            public string Path { get; }
    
            /// <summary>
            /// 指定使用 3D 拉伸可能在此路径,默认false或0
            /// </summary>
            public bool IsExtrusionOk { get; }
    
            /// <summary>
            /// 指定的宽度或在路径坐标系统中应在使用的最大的 x 坐标
            /// </summary>
            public Emu Width { get; }
    
            /// <summary>
            /// 指定框架的高度或在路径坐标系统中应在使用的最大的 y 坐标
            /// </summary>
            public Emu Height { get; }
        }
    

    我写了预设形状公式引擎,可以从 PresetShapeDefinitions.xml 文件转换为 WPF 的 PathGeometry 可以使用的字符串,当前还没有开源此库。不过通过编写代码的形式也是可以的,详细请看 dotnet OpenXML SDK 形状几何 Geometry 的计算公式含义

    为了简化代码,在 GetPresetGeometryPath 方法里面使用的是我用公式引擎计算的输出字符串

            private ShapePath[] GetPresetGeometryPath(ShapeTypeValues presetValue, EmuSize elementSize)
            {
                if (presetValue != ShapeTypeValues.Cube)
                {
                    throw new ArgumentException($"本代码仅支持立方体");
                }
    
                var shapePathList = new ShapePath[4];
    
                // 没有想着使用 elementSize 哈,假设都是固定的大小
    
                // M 0.000,31.920 L 95.760,31.920 L 95.760,127.680 L 0.000,127.680 z
                // FillMode	Norm
                shapePathList[0] = new ShapePath("M 0.000,31.920 L 95.760,31.920 L 95.760,127.680 L 0.000,127.680 z",
                    isStroke: false);
    
                // M 95.760,31.920 L 127.680,0.000 L 127.680,95.760 L 95.760,127.680 z
                // FillMode	DarkenLess
                shapePathList[1] = new ShapePath("M 95.760,31.920 L 127.680,0.000 L 127.680,95.760 L 95.760,127.680 z", PathFillModeValues.DarkenLess, false);
    
                // M 0.000,31.920 L 31.920,0.000 L 127.680,0.000 L 95.760,31.920 z
                // FillMode	LightenLess
                shapePathList[2] = new ShapePath("M 0.000,31.920 L 31.920,0.000 L 127.680,0.000 L 95.760,31.920 z", PathFillModeValues.LightenLess, false);
    
                // M 0.000,31.920 L 31.920,0.000 L 127.680,0.000 L 127.680,95.760 L 95.760,127.680 L 0.000,127.680 z M 0.000,31.920 L 95.760,31.920 L 127.680,0.000 M 95.760,31.920 L 95.760,127.680
                // FillMode	None
                shapePathList[3] = new ShapePath("M 0.000,31.920 L 31.920,0.000 L 127.680,0.000 L 127.680,95.760 L 95.760,127.680 L 0.000,127.680 z M 0.000,31.920 L 95.760,31.920 L 127.680,0.000 M 95.760,31.920 L 95.760,127.680", PathFillModeValues.None);
                return shapePathList;
            }
    

    通过以下代码获取 ShapePath 列表

                var shapePathList = GetPresetGeometryPath(presetGeometry!.Preset!.Value, elementSize);
    

    接下来是本文的核心逻辑,准备绘制不同的亮度的颜色。在开始之前,新建 DrawingGroup 用于拿到 DrawingContext 进行绘制

                var drawingVisual = new DrawingGroup();
                var drawingContext = drawingVisual.Open();
    

    默认的 Cube 填充色是 #FF4472C4 可以通过主题的以下代码拿到

      <!-- Slide1.xml -->
              <a:fillRef idx="1">
                <a:schemeClr val="accent1" />
              </a:fillRef>
    
      <!-- Theme1.xml -->
      <a:themeElements>
        <a:clrScheme name="Office">
          <a:accent1>
            <a:srgbClr val="4472C4" />
          </a:accent1>
        </a:clrScheme>
      </a:themeElements>
    

    本文这里就写固定的值,如果大家想测试不同的课件的颜色效果,还请自己更改

                var fillBrush = new SolidColorBrush((Color) ColorConverter.ConvertFromString("#4472C4"));
    

    通过此画刷绘制几何底色。绘制之前需要拿到底色绘制用的 PathGeometry 对象,如下面代码

                // 创建底色几何
                var pathGeometry = BuildShapePathGeometry(shapePathList);
    
            private PathGeometry BuildShapePathGeometry(ShapePath[] shapePathList)
            {
                PathGeometry? result = null;
                foreach (var shapePath in shapePathList)
                {
                    if (shapePath.FillMode is PathFillModeValues.None && !shapePath.IsStroke)
                    {
                        // 不是可填充的,而且不是线条的,啥都不做
                        continue;
                    }
    
                    var geometry = Geometry.Parse(shapePath.Path);
                    if (result is null)
                    {
                        result = PathGeometry.CreateFromGeometry(geometry);
                    }
                    else
                    {
                        result = Geometry.Combine(result, geometry, GeometryCombineMode.Union, Transform.Identity);
                    }
                }
    
                return result!;
            }     
    

    有很多形状不是多路径的,只有如 Cube 这样有多个 Path 的多路径的才需要绘制底色,绘制底色的代码如下

                // 只有多路径下才先绘制底色
                drawingContext.DrawGeometry(fillBrush, null, pathGeometry);
    

    接下来就是将各个 Path 进行绘制,代码如下

                foreach (var shapePath in shapePathList)
                {
                    if (!shapePath.IsFilled && !shapePath.IsStroke) continue;
    
                    var geometry = GetPathGeometry(shapePath);
                    if (geometry == null)
                    {
                        continue;
                    }
    
                    var brush = GetShapeFillBrush(shapePath, fillBrush);
                    // 忽略线条
                    Pen? pen = null;
                    drawingContext.DrawGeometry(brush, pen, geometry);
                }
    

    从 ShapePath 转换为 Geometry 的代码如下,需要考虑在一个 ShapePath 的字符串里面包含多个 M 起点

            private PathGeometry? GetPathGeometry(ShapePath shapePath)
            {
                var pathGeometry = new PathGeometry();
                var pathFigureCollection = new PathFigureCollection();
                var path = shapePath.Path;
                if (!string.IsNullOrEmpty(path))
                {
                    var pathFigures = PathFigureCollection.Parse(path);
                    if (!pathFigures.Any())
                    {
                        return null;
                    }
                    foreach (var pathFigure in pathFigures)
                    {
                        pathFigure.IsFilled = shapePath.IsFilled;
                        pathFigureCollection.Add(pathFigure);
                    }
                }
                pathGeometry.Figures = pathFigureCollection;
                return pathGeometry;
            }
    

    获取各段的 Geometry 的画刷,通过 GetShapeFillBrush 方法,此方法是本文的核心逻辑,代码如下

            private SolidColorBrush? GetShapeFillBrush(ShapePath shapePath, SolidColorBrush fillBrush, bool isMultiPath = true)
            {
                if (shapePath.IsFilled is false)
                {
                    return null;
                }
    
                switch (shapePath.FillMode)
                {
                    case PathFillModeValues.Norm:
                    {
                        if (isMultiPath)
                        {
                            // 多路径下,不需要重复绘制,绘制内容和底色相同。但是底色已绘制,因此啥都不用做
                            return null;
                        }
                        else
                        {
                            return fillBrush;
                        }
                    }
                    case PathFillModeValues.None:
                        return null;
                    case PathFillModeValues.Darken:
                        return GetOrCreate("#64000000");
                    case PathFillModeValues.DarkenLess:
                        return GetOrCreate("#32000000");
                    case PathFillModeValues.Lighten:
                        return GetOrCreate("#64FFFFFF");
                    case PathFillModeValues.LightenLess:
                        return GetOrCreate("#32FFFFFF");
                    default:
                        throw new ArgumentOutOfRangeException();
                }
    
                static SolidColorBrush GetOrCreate(string color)
                {
                    return new SolidColorBrush((Color) ColorConverter.ConvertFromString(color));
                }
            }
    

    通过覆盖几何加上半透明的画刷,可以让 WPF 自动给底色修改亮度。感谢 晓嗔戈 提供的算法

    接着就是将上面画的放在界面上

                var element = new Border()
                {
                    Width = elementSize.Width.ToPixel().Value,
                    Height = elementSize.Height.ToPixel().Value,
    
                    Margin = new Thickness(10, 10, 10, 10),
                    Background = new DrawingBrush(drawingVisual)
                };
                AddElement(element);
    
            private void AddElement(UIElement element)
            {
                Grid.Children.Clear();
                Grid.Background = null;
                Grid.Children.Add(element);
            }
    

    以上就是本文的代码,运行代码可以看到上图的效果

    本文所有代码和测试文件放在 githubgitee 欢迎访问

    可以通过如下方式获取本文代码

    先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

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

    以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

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

    获取代码之后,打开 Pptx.sln 文件

    本文的属性是依靠 dotnet OpenXML 解压缩文档为文件夹工具 工具协助测试的,这个工具是开源免费的工具,欢迎小伙伴使用

    更多请看 Office 使用 OpenXML SDK 解析文档博客目录

    后续解析引擎也许会开源,欢迎关注我所在团队的开源组织 https://github.com/dotnet-campus 里面有很多开源项目

    博客园博客只做备份,博客发布就不再更新,如果想看最新博客,请到 https://blog.lindexi.com/

    知识共享许可协议
    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我[联系](mailto:lindexi_gd@163.com)。
  • 相关阅读:
    Eclipse 远程调试
    大数据处理方法bloom filter
    sicily 1259 Sum of Consecutive Primes
    sicily 1240. Faulty Odometer
    sicily 1152 简单马周游 深度优先搜索及回溯算法
    sicily 1050 深度优先搜索解题
    sicily 1024 邻接矩阵与深度优先搜索解题
    sicily 1156 二叉树的遍历 前序遍历,递归,集合操作
    sicily 1443 队列基本操作
    sicily 1006 team rankings 枚举解题
  • 原文地址:https://www.cnblogs.com/lindexi/p/15502114.html
Copyright © 2011-2022 走看看