使用 Open XML SDK 向 PowerPoint 插入可播放的动画 3D 模型(Animated Model 3D)

  • 后端
  • 开发工具

【免费下载链接】Open-XML-SDK

Open XML SDK by Microsoft

项目地址: https://gitcode.com/gh_mirrors/op/Open-XML-SDK
点击查看 免费下载

本指南基于 Open-XML-SDK 仓库中的 samples/AnimatedModel3DExample 示例,完整讲解如何通过 C# 与 Open XML SDK 向 .pptx 演示文稿插入一个可播放的动画 3D 模型:既包含嵌入 .glb(glTF 二进制 3D 文件)与 .png(3D 图像预览图)两个部件,也包含构造 mc:AlternateContent(旧版 PowerPoint 回退为普通图片)与 p:timing(动画时间轴)的完整流程。读完本文,你将掌握动画 3D 模型在 OOXML 包结构中的落盘方式、SDK 对应强类型元素的使用方法,以及一个可直接运行的命令行示例程序。

一、示例概览与前置条件

在 samples/AnimatedModel3DExample/README.md 中,微软官方对该示例的描述非常精炼:

This sample code shows how to add an Animated Model 3D to a PowerPoint file. When adding an Animated Model 3D to a presentation, the SDK needs a .png file of the 3D image and a .glb file, which is a 3D file format for rendering in 3D.

示例目录结构如下:

  • Program.cs:示例唯一入口,包含全部插入逻辑;
  • AddAnimatedModel3D.csproj:示例工程文件,引用 src\DocumentFormat.OpenXml\DocumentFormat.OpenXml.csproj;
  • README.md:说明文档。

向 PowerPoint 插入动画 3D 模型需要两类素材文件:

文件格式说明用途
.png3D 模型的 2D 图像作为模型在幻灯片中的视觉预览/贴图(r:embed),同时也是旧版 PowerPoint 回退分支中的图片来源
.glbglTF Binary(GL Transmission Format 的二进制封装)真正的 3D 模型数据,内嵌动画;SDK 将其写入 Model3DReferenceRelationshipPart,MIME 类型为 model/gltf.binary

由于 3D 模型特性属于 Office 2019 引入的扩展(相关命名空间为 http://schemas.microsoft.com/office/drawing/2017/model3d,即 am3d),因此最终生成的演示文稿需要 PowerPoint 2019 及以上版本才能播放完整动画;在旧版本中,幻灯片会展示 mc:Fallback 分支中的静态图片。

二、示例工程与运行方式

AddAnimatedModel3D.csproj 非常简单,核心是引用仓库主库项目:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFrameworks>$(SamplesFrameworks)</TargetFrameworks>
	</PropertyGroup>
	<ItemGroup>
		<ProjectReference Include="..\..\src\DocumentFormat.OpenXml\DocumentFormat.OpenXml.csproj" />
	</ItemGroup>
</Project>

其中 $(SamplesFrameworks) 由根目录 Directory.Build.props 定义:在默认(非 net35/net40 专项构建)情况下为 net8.0;net10.0,若在 Windows 上还额外追加 net472。示例目录共享 samples/Directory.Build.targets,它会自动编译 samples/common/*.cs 并引用 src 下两个核心工程。

从仓库根目录运行:

dotnet run --project samples/AnimatedModel3DExample/AddAnimatedModel3D.csproj -- <OOXML> <png> <glb>

其中:

  • <OOXML>:目标 .pptx 文件路径(会被就地修改);
  • <png>:3D 模型的 .png 图像文件路径;
  • <glb>:3D 动画模型的 .glb 文件路径。

Main 方法会先校验参数个数,再调用公共工具类 Common.ExampleUtilities.CheckIfFilesExist(见 samples/common/ExampleUtilities.cs)逐个检查文件是否存在,全部通过后才执行插入逻辑:

private static void Main(string[] args)
{
    if (args.Length < 3)
    {
        Common.ExampleUtilities.ShowHelp(
            new string[]
            {
                "AnimatedModel3DExample: ",
                "Usage: AnimatedModel3DExample <OOXML> <png> <glb>",
                "Where <OOXML> is the file to add the animated model 3d to",
                "and <png> is the .png file with the image for the Animated Model 3D",
                "Where <glb> is the .glb file for the animated model 3d",
            });
    }
    else if (Common.ExampleUtilities.CheckIfFilesExist(args))
    {
        InsertAnimatedModel3D(args[0], args[1], args[2]);
    }
}

整个示例的核心方法正如 README 所强调的:

public static void InsertAnimatedModel3D(string pptxPath, string pngPath, string glbPath)

三、插入流程总览

InsertAnimatedModel3D(完整实现见 samples/AnimatedModel3DExample/Program.cs)按如下步骤工作:

  1. 打开演示文稿:以可读写方式 PresentationDocument.Open(pptxPath, true) 打开;
  2. 定位第一张幻灯片:通过 PresentationPart.Presentation.SlideIdList 取得第一个 SlideId 的关系 ID,再用 GetPartById 拿到 SlidePart;
  3. 嵌入两个部件:把 .glb 写入 Model3DReferenceRelationshipPart,把 .png 写入 ImagePart,并各自生成新的 rId;
  4. 构造 mc:AlternateContent:在幻灯片的 CommonSlideData.ShapeTree 上追加 AlternateContent,其中:
    • mc:Choice(Requires="am3d"):完整的 3D 模型 GraphicFrame,包含模型几何、相机、光照、贴图与动画扩展;
    • mc:Fallback:一张普通 Picture,旧版 PowerPoint 显示此分支;
  5. 追加 p:timing:在 SlidePart.RootElement(p:sld)上追加动画时间轴,驱动 embedded1 属性在 0→1 间线性变化,实现模型的嵌入动画播放。

下面按步骤逐层深入。

四、打开文档并定位目标幻灯片

// Open the presentation document
using PresentationDocument presentationDocument = PresentationDocument.Open(pptxPath, true);

// Get the relationship ID of the first slide.
PresentationPart presentationPart = presentationDocument.PresentationPart;

if (presentationPart != null && presentationPart.Presentation != null)
{
    OpenXmlElementList slideIds = presentationPart.Presentation.SlideIdList.ChildElements;

    if (slideIds.Count > 0)
    {
        string relId = (slideIds[0] as SlideId).RelationshipId;
        SlidePart slidePart = (SlidePart)presentationPart.GetPartById(relId);
        int partsCount = slidePart.Parts.Count();
        ...
    }
}

要点说明:

  • PresentationDocument.Open(pptxPath, true) 的第二个参数 editable 为 true,表示以读写模式打开,后续所有修改会直接写回原文件;
  • Presentation.SlideIdList 是演示文稿中幻灯片的引用列表,SlideId.RelationshipId 指向 PresentationPart 与对应 SlidePart 之间的关系;
  • slidePart.Parts.Count() 用于计算当前幻灯片已拥有的部件数量,作为生成新关系 ID 的基准;
  • 示例将 3D 模型插入第一张幻灯片,实际项目中你可以遍历 SlideIdList 选择任意目标幻灯片。

五、嵌入 .glb 与 .png 部件

// Add the .png and .glb files for the 3D image
using FileStream glbFileStream = new FileStream(glbPath, FileMode.Open, FileAccess.Read);
glbFileStream.Seek(0, SeekOrigin.Begin);
string glbId = $"rId{++partsCount}";
Model3DReferenceRelationshipPart glbImagePart = slidePart.AddNewPart<Model3DReferenceRelationshipPart>(glbId);
glbImagePart.FeedData(glbFileStream);

using FileStream pngFileStream = new FileStream(pngPath, FileMode.Open, FileAccess.Read);
pngFileStream.Seek(0, SeekOrigin.Begin);
string pngId = $"rId{++partsCount}";
ImagePart pngImagePart = slidePart.AddImagePart(ImagePartType.Png, pngId);
pngImagePart.FeedData(pngFileStream);

这段代码体现了 Open XML SDK 部件模型的典型用法:

  • AddNewPart<Model3DReferenceRelationshipPart>(glbId) 在 SlidePart 下新建 3D 模型引用部件,并指定关系 ID(rId 从现有部件数递增,避免冲突);
  • AddImagePart(ImagePartType.Png, pngId) 新建 PNG 图片部件;
  • 两个部件都通过 FeedData(Stream) 直接把文件流写入包(ZIP)内。

在 SDK 底层,Model3DReferenceRelationshipPart 被定义为 model/gltf.binary 的内容类型。源码 src/DocumentFormat.OpenXml/Packaging/Model3DReferenceRelationshipPart.cs 中的实现印证了这一点:

public partial class Model3DReferenceRelationshipPart
{
    /// <inheritdoc/>
    private protected override bool IsValidContentType(IPackagePart part)
    {
        if (base.IsValidContentType(part))
        {
            return true;
        }

        // model/gltf.binary is an acceptable MIME for this part, PowerPoint writes this on save.
        return part.ContentType == "model/gltf.binary";
    }
}

从源码注释可以看出:model/gltf.binary 是 PowerPoint 保存文件时实际使用的 MIME 类型,SDK 在读取时也兼容这种写法,这正是 .glb 文件在 OOXML 包中的标准落盘形态。ImagePartType.Png 则属于 ImagePart 支持的常规类型之一,其 MIME 为 image/png。

六、构造 mc:AlternateContent:3D 模型与回退图片

拿到 CommonSlideData 后,示例在其 ShapeTree 上追加 AlternateContent。这是 OOXML Markup Compatibility(mc)机制的典型应用:让同一位置在不同版本的 PowerPoint 中呈现不同内容。

CommonSlideData commonSlideDataPart = slidePart.RootElement.Descendants<CommonSlideData>().FirstOrDefault();

// Add an AlternateContent element to the shape tree
AlternateContent alternateContent = commonSlideDataPart.ShapeTree.AppendChild(new AlternateContent());

// Add mc:Choice element to AlternateContent
AlternateContentChoice alternateContentChoice = alternateContent.AppendChild(new AlternateContentChoice());
alternateContentChoice.Requires = "am3d";
alternateContentChoice.AddNamespaceDeclaration("am3d", "http://schemas.microsoft.com/office/drawing/2017/model3d");

// Add mc:Fallback element to AlternateContent
AlternateContentFallback alternateContentFallback = alternateContent.AppendChild(new AlternateContentFallback());

关键设置:

  • AlternateContentChoice.Requires = "am3d":声明该分支要求支持 am3d 命名空间,只有理解 3D 模型标记的 PowerPoint(2019+)才会选用;
  • AddNamespaceDeclaration("am3d", "http://schemas.microsoft.com/office/drawing/2017/model3d"):为 mc:Choice 显式声明 3D 模型命名空间;
  • AlternateContentFallback:作为通用回退分支,任何版本的 PowerPoint 都能解析其中的普通绘图元素。

6.1 mc:Choice 分支:完整的 3D 模型 GraphicFrame

mc:Choice 内嵌一个 GraphicFrame,其 XML 结构大致为:

p:graphicFrame
├─ p:nvGraphicFramePr
│  ├─ p:cNvPr            (id="2" name="3D Model 1" descr="Flying bee")
│  │  └─ a:extLst → a:ext (a16:creationId) uri={FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}
│  ├─ p:cNvGraphicFramePr
│  └─ p:nvPr
│     └─ a:extLst → a:ext (p14:modId) uri={D42A27DB-BD31-4B8C-83A1-F6EECF244321}
├─ p:xfrm
│  ├─ a:off (x="4016654" y="1698584")
│  └─ a:ext (cx="4158691" cy="3460830")
└─ a:graphic
   └─ a:graphicData (uri="http://schemas.microsoft.com/office/drawing/2017/model3d")
      └─ am3d:model3d (r:embed="<glbId>")
         ├─ am3d:spPr          (2D 变换 + 矩形预设几何)
         ├─ am3d:camera        (透视投影、观察点、上向量)
         ├─ am3d:transform     (比例、平移、旋转)
         ├─ am3d:raster (r:embed="<pngId>" rName="Office3DRenderer" rVer="16.0.8326")
         ├─ am3d:extLst
         │  ├─ a:ext (a3danim:embedAnim)  uri={9A65AA19-BECB-4387-8358-8AD5134E1D82}
         │  └─ a:ext (a3danim:posterFrame) uri={E9DE012E-A134-456F-84FE-255F9AAD75C6}
         ├─ am3d:objViewport (viewportSz="5418666")
         ├─ am3d:ambientLight (环境光)
         └─ am3d:ptLight ×3   (三盏点光源)

示例通过 GraphicFrame 的强类型构造函数把整棵元素树一次性构建出来,核心片段如下:

alternateContentChoice.AppendChild(
    new GraphicFrame(
        new NonVisualGraphicFrameProperties(
            new NonVisualDrawingProperties(
                new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtensionList(
                    new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtension(
                        creationId)
                    { Uri = "{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}" }))
            { Id = threeDModelId, Name = "3D Model 1", Description = "Flying bee" },
            new NonVisualGraphicFrameDrawingProperties(),
            new ApplicationNonVisualDrawingProperties(
                new ApplicationNonVisualDrawingPropertiesExtensionList(
                    new ApplicationNonVisualDrawingPropertiesExtension(
                        modificationId)
                    { Uri = "{D42A27DB-BD31-4B8C-83A1-F6EECF244321}" }))),
        new Transform(
            offset,
            aExtension),
        new DocumentFormat.OpenXml.Drawing.Graphic(
            new DocumentFormat.OpenXml.Drawing.GraphicData(
                new DocumentFormat.OpenXml.Office2019.Drawing.Model3D.Model3D(...)
                { Embed = glbId })
            { Uri = "http://schemas.microsoft.com/office/drawing/2017/model3d" })));

其中 Model3D 的 Embed = glbId 通过 r:embed 属性把第 5 步嵌入的 .glb 部件关联到模型元素上。这些强类型元素均由 SDK 的源码生成器从官方 Schema 自动产出(见 generated/DocumentFormat.OpenXml/.../schemas_microsoft_com_office_drawing_2017_model3d.g.cs),例如 Model3D 类:

public partial class Model3D : OpenXmlCompositeElement
{
    internal static readonly new OpenXmlQualifiedName ElementQName =
        new("http://schemas.microsoft.com/office/drawing/2017/model3d", "model3d");
    ...
    public StringValue? Embed
    {
        get => GetAttribute<StringValue>();
        set => SetAttribute(value);
    }
    ...
    internal override void ConfigureMetadata(ElementMetadata.Builder builder)
    {
        ...
        builder.Availability = FileFormatVersions.Office2019;
        ...
    }
}

从生成代码的 ConfigureMetadata 可以看到 Model3D 的元素可用性被标记为 FileFormatVersions.Office2019,其合法子元素顺序为 spPr → camera → transform → (可选 attributionSourceURL) → (可选 raster) → (可选 extLst) → (objViewport | winViewport) → (可选 ambientLight) → (可选一组点光/聚光/方向光/未知光)。示例中的元素顺序完全遵守这一粒子(Particle)约束,这正是 SDK 校验与序列化的依据。

Model3D 内部还细分了几组有明确分工的子元素(示例中都有体现):

  • 几何与外观:ShapeProperties(am3d:spPr,矩形预设几何 + 2D 变换)、Model3DCamera(am3d:camera,包含 PosPoint3D、UpVector3D、LookAtPoint3D 与 PerspectiveProjection{Fov=2700000})、Model3DTransform(am3d:transform,米/模型单位比率、平移、缩放、旋转);
  • 贴图与动画扩展:Model3DRaster(am3d:raster,Embed = pngId 引用 PNG 预览图,并标注渲染器 Office3DRenderer 与版本 16.0.8326)、Model3DExtensionList(挂接动画扩展);
  • 视口与光照:ObjectViewport(ViewportSz = 5418666)、AmbientLight(50% 灰环境光)、三盏 PointLight(暖橙、冷蓝、紫青三色点光源,各自带 Rad = 0)。

6.2 mc:Fallback 分支:普通图片

为了让不支持 am3d 的 PowerPoint 版本仍能显示内容,mc:Fallback 中放了一张与 3D 模型位置、大小一致的普通 Picture,其图片即第 5 步嵌入的 PNG:

alternateContentFallback.AppendChild(
    new DocumentFormat.OpenXml.Presentation.Picture(
        new NonVisualPictureProperties(
            new NonVisualDrawingProperties(
                new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtensionList(
                    new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtension(
                        creationId2)
                    { Uri = "{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}" }))
            { Id = threeDModelId, Name = "3D Model 1", Description = "Flying bee" },
            new DocumentFormat.OpenXml.Presentation.NonVisualPictureDrawingProperties(
                new DocumentFormat.OpenXml.Drawing.PictureLocks()
                {
                    NoGrouping = true, NoRotation = true, NoChangeAspect = true,
                    NoMove = true, NoResize = true, NoEditPoints = true,
                    NoAdjustHandles = true, NoChangeArrowheads = true,
                    NoChangeShapeType = true, NoCrop = true
                }),
            new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()),
        new DocumentFormat.OpenXml.Presentation.BlipFill(
            new DocumentFormat.OpenXml.Drawing.Blip() { Embed = pngId },
            new DocumentFormat.OpenXml.Drawing.Stretch(
                new DocumentFormat.OpenXml.Drawing.FillRectangle())),
        new DocumentFormat.OpenXml.Presentation.ShapeProperties(
            new DocumentFormat.OpenXml.Drawing.Transform2D(
                offset2,
                aExtension3),
            new DocumentFormat.OpenXml.Drawing.PresetGeometry(
                new DocumentFormat.OpenXml.Drawing.AdjustValueList())
            { Preset = DocumentFormat.OpenXml.Drawing.ShapeTypeValues.Rectangle })));

细节说明:

  • 回退图片的 Blip.Embed = pngId 与 3D 分支共用同一个 PNG 部件,无需额外资源;
  • PictureLocks 上几乎所有锁都置为 true,防止用户在旧版 PowerPoint 中移动、缩放或编辑回退图片;
  • creationId2 是 creationId 的克隆(Clone()),保证两个分支持有相同内容的 a16:creationId,使 PowerPoint 能识别二者是同一对象的两种表达;
  • offset2、aExtension3 同样来自 Offset/Extension 的克隆,确保回退图片与 3D 模型的包围盒(位置 4016654, 1698584、尺寸 4158691×3460830,EMU 单位)完全一致。

七、辅助元素:creationId、modId 与动画扩展

在构建 GraphicFrame 之前,示例先准备了几个跨命名空间的辅助元素:

// Create a creationId with the correct namespace
string guidId = "{" + System.Guid.NewGuid().ToString().ToUpper() + "}";
DocumentFormat.OpenXml.Office2016.Drawing.CreationId creationId = new DocumentFormat.OpenXml.Office2016.Drawing.CreationId();
creationId.AddNamespaceDeclaration("a16", "http://schemas.microsoft.com/office/drawing/2014/main");
creationId.Id = guidId;

// Clone the creationId
DocumentFormat.OpenXml.Office2016.Drawing.CreationId creationId2 =
    (DocumentFormat.OpenXml.Office2016.Drawing.CreationId)creationId.Clone();
string creationIdPartenId = guidId;
UInt32Value threeDModelId = new UInt32Value(2U);

// Create modId with the correct namespace xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main"
DocumentFormat.OpenXml.Office2010.PowerPoint.ModificationId modificationId =
    new DocumentFormat.OpenXml.Office2010.PowerPoint.ModificationId();
modificationId.AddNamespaceDeclaration("p14", "http://schemas.microsoft.com/office/powerpoint/2010/main");
modificationId.Val = 3636546711;

// Create an a:Extension with the correct attributes
DocumentFormat.OpenXml.Drawing.Extension aExtension = new DocumentFormat.OpenXml.Drawing.Extension();
aExtension.SetAttributes(
    new OpenXmlAttribute[]
    {
        new OpenXmlAttribute("cx", string.Empty, "4158691"),
        new OpenXmlAttribute("cy", string.Empty, "3460830"),
    });
DocumentFormat.OpenXml.Drawing.Extension aExtension2 = (DocumentFormat.OpenXml.Drawing.Extension)aExtension.Clone();
DocumentFormat.OpenXml.Drawing.Extension aExtension3 = (DocumentFormat.OpenXml.Drawing.Extension)aExtension.Clone();

// Create a a3danim:posterFrame and add the namespace
DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.PosterFrame model3dPosterFrame =
    new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.PosterFrame() { AnimId = 0 };
model3dPosterFrame.AddNamespaceDeclaration("a3danim", "http://schemas.microsoft.com/office/drawing/2018/animation/model3d");

// Create a:off
DocumentFormat.OpenXml.Drawing.Offset offset = new DocumentFormat.OpenXml.Drawing.Offset() { X = 4016654, Y = 1698584 };
DocumentFormat.OpenXml.Drawing.Offset offset2 = (DocumentFormat.OpenXml.Drawing.Offset)offset.Clone();

// Create a3danim:embedAnim and assign its namespace
DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.EmbeddedAnimation embeddedAnimation =
    new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.EmbeddedAnimation() { AnimId = 0 };
embeddedAnimation.AddNamespaceDeclaration("a3danim", "http://schemas.microsoft.com/office/drawing/2018/animation/model3d");
embeddedAnimation.AppendChild(
    new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.AnimationProperties()
    { Length = "1899", Count = "indefinite" });

这些元素的作用分别是:

  • a16:creationId:对象的全局唯一创建 ID(GUID),同一对象在 Choice 与 Fallback 分支中保持一致;
  • p14:modId:修改 ID,值为 3636546711,用于对象版本追踪;
  • a:ext(cx/cy 属性):通过 SetAttributes 直接写入扩展属性,供图形框与图片共用尺寸信息;Clone() 让三份拷贝保持一致;
  • a3danim:embedAnim 与 a3danim:posterFrame:动画嵌入与海报帧扩展,挂到 Model3DExtensionList 的两个 a:ext 中(URI 分别为 {9A65AA19-BECB-4387-8358-8AD5134E1D82} 与 {E9DE012E-A134-456F-84FE-255F9AAD75C6}),AnimationProperties 指定动画时长 1899ms 且循环次数为 indefinite。

EmbeddedAnimation 与 PosterFrame 均来自 DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D 命名空间(对应 a3danim Schema),同样由源码生成器产出,见 generated/DocumentFormat.OpenXml/.../schemas_microsoft_com_office_drawing_2018_animation_model3d.g.cs。

八、追加 p:timing 动画时间轴

最后一步,在 SlidePart.RootElement(p:sld)上追加 Timing 元素,构造完整的动画时间轴,让 3D 模型真正“动起来”:

slidePart.RootElement.AppendChild(
    new DocumentFormat.OpenXml.Presentation.Timing(
        new DocumentFormat.OpenXml.Presentation.TimeNodeList(
            new DocumentFormat.OpenXml.Presentation.ParallelTimeNode(
                new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
                    new DocumentFormat.OpenXml.Presentation.ChildTimeNodeList(
                        new DocumentFormat.OpenXml.Presentation.SequenceTimeNode(
                            new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
                                new DocumentFormat.OpenXml.Presentation.ChildTimeNodeList(
                                    new DocumentFormat.OpenXml.Presentation.ParallelTimeNode(
                                        new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
                                            new DocumentFormat.OpenXml.Presentation.StartConditionList(
                                                new DocumentFormat.OpenXml.Presentation.Condition() { Delay = "indefinite" },
                                                new DocumentFormat.OpenXml.Presentation.Condition(
                                                    new DocumentFormat.OpenXml.Presentation.TimeNode() { Val = 2 })
                                                { Event = DocumentFormat.OpenXml.Presentation.TriggerEventValues.OnBegin, Delay = "0" }),
                                            ...
                                            new DocumentFormat.OpenXml.Presentation.Animate(
                                                new DocumentFormat.OpenXml.Presentation.CommonBehavior(
                                                    new DocumentFormat.OpenXml.Presentation.CommonTimeNode()
                                                    { Id = 6, Duration = "1900", Fill = TimeNodeFillValues.Hold },
                                                    new DocumentFormat.OpenXml.Presentation.TargetElement(
                                                        new DocumentFormat.OpenXml.Presentation.ShapeTarget() { ShapeId = "2" }),
                                                    new DocumentFormat.OpenXml.Presentation.AttributeNameList(
                                                        new DocumentFormat.OpenXml.Presentation.AttributeName("embedded1"))),
                                                new DocumentFormat.OpenXml.Presentation.TimeAnimateValueList(
                                                    new TimeAnimateValue(
                                                        new VariantValue(
                                                            new FloatVariantValue() { Val = SingleValue.FromSingle(0) }))
                                                    { Time = "0" },
                                                    new TimeAnimateValue(
                                                        new VariantValue(
                                                            new FloatVariantValue() { Val = SingleValue.FromSingle(1) }))
                                                    { Time = "100000" }))
                                            { CalculationMode = AnimateBehaviorCalculateModeValues.Linear,
                                              ValueType = AnimateBehaviorValues.Number })
                                        ...
                                    { Id = 4, Fill = TimeNodeFillValues.Hold })
                            ...))
                    { Id = 3, Fill = TimeNodeFillValues.Hold })))
                { Id = 2, Duration = "indefinite", NodeType = TimeNodeValues.MainSequence },
                ...
            { Concurrent = true, NextAction = NextActionValues.Seek }))
    { Id = 1, Duration = "indefinite", Restart = TimeNodeRestartValues.Never, NodeType = TimeNodeValues.TmingRoot }));

时间轴的关键信息可以概括为以下结构树:

p:timing (id=1, dur=indefinite, nodeType=tmingRoot)
└─ p:tnLst
   └─ p:par (id=2, dur=indefinite, nodeType=mainSequence, concurrent=true)
      └─ p:cTn → p:childTnLst
         └─ p:seq (nextAc=seek)
            └─ p:cTn (id=3, fill=hold)
               └─ p:childTnLst
                  └─ p:par (id=4, fill=hold)
                     └─ p:cTn (id=5, presetId=100, presetClass=emphasis,
                               presetSubtype=1, repeatCount=indefinite,
                               fill=hold, nodeType=withEffect)
                        └─ p:childTnLst
                           └─ p:par → p:cTn (startCond delay=0)
                              └─ p:childTnLst
                                 └─ p:anim (calcMode=linear, valueType=number)
                                    ├─ p:cBhvr → p:cTn (id=6, dur=1900, fill=hold)
                                    │  └─ p:tgtEl → p:spTgt (spid="2")
                                    │     └─ p:attrNameLst → p:attrName "embedded1"
                                    └─ p:tavLst
                                       ├─ p:tav (tm=0)      → float 0
                                       └─ p:tav (tm=100000) → float 1

要点解读:

  • 目标对象:ShapeTarget.ShapeId = "2" 指向第 2 个形状,即刚插入的 3D 模型 GraphicFrame(其 Id = threeDModelId = 2);
  • 动画属性:AttributeName("embedded1") 驱动模型内嵌动画的播放进度属性,数值从 0(Time = "0")线性过渡到 1(Time = "100000",OOXML 时间基准为 1000 单位/秒,即 100000/1000 = 100 秒);CalculationMode = Linear 表示线性插值;
  • 循环与触发:RepeatCount = "indefinite" 让动画无限循环,NodeType = WithEffect、PresetClass = Emphasis 表示强调类动画效果;
  • 主时间线:根节点 nodeType = TmingRoot、主序列 nodeType = MainSequence,StartConditionList 中 Event = OnBegin 使动画在幻灯片播放时自动开始,无需点击。

示例中有一行被注释掉的代码 AttributeName.InnerXml = "embedded1"(见 Program.cs),它展示了构造 attrName 元素的另一种方式,正式代码则采用强类型 AttributeName("embedded1") 构造函数,两者效果等价。

九、底层原理:从 Schema 到强类型元素

通过上面的代码可以看出,整个示例几乎完全基于 SDK 的强类型 DOM API,而不是手写 XML。这些强类型类全部由仓库中的源码生成器(DocumentFormat.OpenXml.Generator)从官方 OOXML Schema 自动生成,其元素命名空间、粒子约束与版本可用性都编码在生成代码中。

以 Model3D 为例,在 schemas_microsoft_com_office_drawing_2017_model3d.g.cs 中可以清晰看到:

  • 元素限定名:{http://schemas.microsoft.com/office/drawing/2017/model3d}model3d;
  • 版本可用性:builder.Availability = FileFormatVersions.Office2019——这与“动画 3D 模型需要 Office 2019+”的事实一致;
  • 子元素粒子序列:spPr → camera → transform → (attributionSourceURL)? → (raster)? → (extLst)? → (objViewport|winViewport) → (ambientLight)? → (一组灯光)?,示例代码的子元素顺序与该序列严格对应。

同样的生成逻辑也覆盖了 a3danim(schemas_microsoft_com_office_drawing_2018_animation_model3d.g.cs)、a16:creationId(Office2016.Drawing)与 p14:modId(Office2010.PowerPoint)等类型。这意味着:只要遵循粒子约束构造元素树,SDK 就能在保存时输出 PowerPoint 认可的合法 OOXML,同时这些元数据也支撑着 SDK 的验证(Validation)与 FileFormatVersions 兼容性检查。

十、运行验证与常见问题

  1. 运行示例:准备好一个现有 .pptx、一张模型预览 .png 和一个 .glb 模型文件,然后执行:

    dotnet run --project samples/AnimatedModel3DExample/AddAnimatedModel3D.csproj -- demo.pptx model.png model.glb
    

    程序会就地修改 demo.pptx,并在第一张幻灯片中插入 3D 模型及其动画。

  2. 验证结果:用 PowerPoint 2019 及以上版本打开生成的文件,应能看到 3D 模型呈现并自动循环播放动画;若用旧版 PowerPoint 或兼容查看器打开,应显示 mc:Fallback 中的静态 PNG 图片。

  3. 常见问题:

    • 文件未找到:命令行参数中的路径不存在时,CheckIfFilesExist 会向标准错误输出 file not found: <path> 并中止执行;
    • 旧版 Office 显示空白:说明回退分支(mc:Fallback)构造不完整或 PNG 部件未正确嵌入,请检查 Blip.Embed = pngId 与 creationId 克隆是否一致;
    • 动画不播放:确认 p:timing 的 ShapeTarget.ShapeId 与 GraphicFrame 的 Id 一致,且 attrName 为 embedded1、两个 tav 关键帧(0/100000)完整;
    • 版本兼容:3D 模型特性依赖 Office 2019+,代码中涉及的 Office2019.Drawing.Model3D、Office2019.Drawing.Animation.Model3D 类型均标记为 FileFormatVersions.Office2019,在更早版本的 PowerPoint 中只会显示回退图片。

十一、小结

本文从 Open-XML-SDK 仓库的 samples/AnimatedModel3DExample 出发,完整拆解了“向 PowerPoint 插入动画 3D 模型”的全过程:嵌入式嵌入 .glb/.png 两个部件、用 mc:AlternateContent 组织 3D 模型与回退图片两种表达、以 a3danim:embedAnim/posterFrame 挂接动画扩展、最后用 p:timing 构造驱动 embedded1 属性的无限循环时间轴。配合源码生成器产出的强类型元素(如 Model3D、EmbeddedAnimation)以及 Model3DReferenceRelationshipPart.cs 中的 MIME 兼容实现,读者可以在此基础上扩展出:向任意幻灯片插入、自定义相机与光照、调整动画时长与循环次数、批量处理多个模型等更丰富的应用场景。

</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>

  • 后端
  • 开发工具

【免费下载链接】Open-XML-SDK

Open XML SDK by Microsoft

项目地址: https://gitcode.com/gh_mirrors/op/Open-XML-SDK
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值