C#使用OpenXML生成一个PPT
PPT格式的生成一直是个难题
相较于Word和PDF,PPT有着严格的格式规定
NPOI官方文档中有操作PPT的具体命名空间
但是由于一些不可抗力因素,现在NPOI已经不能使用了
所以现在用更底层的OpenXML分享一个PPT生成
并且写了个例子方法 SaveFromImages ,传入ppt储存的地址,再传入一个带有图片的文件夹
就会用该文件夹下面的图片生成一个PPT
考虑到代码可能有点生涩难懂,特意让AI尽量在每行都加上了注释
供给大家参考学习
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Presentation;
using D = DocumentFormat.OpenXml.Drawing;
using System;
using System.Collections.Generic;
using System.IO;
namespace OfficeModule
{
/// <summary>
/// 基于 Open XML SDK(DocumentFormat.OpenXml 3.x) 的 pptx 保存工具,不依赖 WPS/Office COM。
/// 内置一份合法的最小 pptx 模板(含母版/版式/主题),生成的文件可被 WPS 与 PowerPoint 正常打开。
/// </summary>
// 标注本类仅支持 Windows:因为 GetImageSize 用到了 System.Drawing.Common(Windows 专属)
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public static class PptxSaver
{
// 默认幻灯片宽度(EMU):9144000 EMU = 10 英寸,对应 4:3 比例
public const int DefaultSlideWidthEmu = 9144000;
// 默认幻灯片高度(EMU):6858000 EMU = 7.5 英寸,对应 4:3 比例
public const int DefaultSlideHeightEmu = 6858000;
/// <summary>
/// 内置最小 pptx 模板(Base64),含完整的母版/版式/主题,保证生成的 pptx 结构合法可打开。
/// </summary>
private const string EmbeddedTemplateBase64 =
@" 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
xuTq6N+H0bXMNnnyn52Ny4xR7+Tb3ugPt9q8m6pjc5P84Fo1181eSxydr8p2vax0+RNQSwECFAAU
AAAACABAg/xcn0y0pyIBAADOAwAAEwAAAAAAAAAAAAAAgAEAAAAAW0NvbnRlbnRfVHlwZXNdLnht
bFBLAQIUABQAAAAIAECD/FwbyrjurgAAACwBAAALAAAAAAAAAAAAAACAAVMBAABfcmVscy8ucmVs
c1BLAQIUABQAAAAIAECD/Fw5/nn9JgEAAEMCAAAUAAAAAAAAAAAAAACAASoCAABwcHQvcHJlc2Vu
dGF0aW9uLnhtbFBLAQIUABQAAAAIAECD/FwjFB9KvQAAALcBAAAfAAAAAAAAAAAAAACAAYIDAABw
cHQvX3JlbHMvcHJlc2VudGF0aW9uLnhtbC5yZWxzUEsBAhQAFAAAAAgAQIP8XHAhA+wWAQAAOwIA
ABUAAAAAAAAAAAAAAIABfAQAAHBwdC9zbGlkZXMvc2xpZGUxLnhtbFBLAQIUABQAAAAIAECD/Fzd
EU1KsgAAADUBAAAgAAAAAAAAAAAAAACAAcUFAABwcHQvc2xpZGVzL19yZWxzL3NsaWRlMS54bWwu
cmVsc1BLAQIUABQAAAAIAECD/FwwEMLBLwEAAG4CAAAhAAAAAAAAAAAAAACAAbUGAABwcHQvc2xp
ZGVMYXlvdXRzL3NsaWRlTGF5b3V0MS54bWxQSwECFAAUAAAACABAg/xca5z8D7EAAAA1AQAALAAA
AAAAAAAAAAAAgAEjCAAAcHB0L3NsaWRlTGF5b3V0cy9fcmVscy9zbGlkZUxheW91dDEueG1sLnJl
bHNQSwECFAAUAAAACABAg/xcCKlfPqEBAAB5AwAAIQAAAAAAAAAAAAAAgAEeCQAAcHB0L3NsaWRl
TWFzdGVycy9zbGlkZU1hc3RlcjEueG1sUEsBAhQAFAAAAAgAQIP8XKpf3CzIAAAAvAEAACwAAAAA
AAAAAAAAAIAB/goAAHBwdC9zbGlkZU1hc3RlcnMvX3JlbHMvc2xpZGVNYXN0ZXIxLnhtbC5yZWxz
UEsBAhQAFAAAAAgAQIP8XHf/pGswAgAARAgAABQAAAAAAAAAAAAAAIABEAwAAHBwdC90aGVtZS90
aGVtZTEueG1sUEsFBgAAAAALAAsALgMAAHIOAAAAAA==";
/// <summary>
/// 把指定文件夹下的所有图片(每张占一页)生成为 pptx。
/// 图片按文件名自然顺序排序,每页一张,等比缩放并居中铺满幻灯片。
/// 支持的图片格式:jpg/jpeg/png/bmp/gif/tif/tiff/wmf/emf。
/// </summary>
/// <param name="destPath">目标 pptx 完整路径(若已存在会被覆盖)</param>
/// <param name="imageFolder">图片所在文件夹完整路径</param>
/// <param name="slideWidthEmu">幻灯片宽度(EMU),默认 9144000(10英寸,4:3)</param>
/// <param name="slideHeightEmu">幻灯片高度(EMU),默认 6858000(7.5英寸)</param>
/// <returns>成功返回 true;无图片或失败返回 false</returns>
public static bool SaveFromImages(string destPath, string imageFolder,
int slideWidthEmu = DefaultSlideWidthEmu, int slideHeightEmu = DefaultSlideHeightEmu)
{
// —— 参数校验 ——
if (string.IsNullOrWhiteSpace(destPath))
throw new ArgumentException("目标路径不能为空", nameof(destPath));
if (string.IsNullOrWhiteSpace(imageFolder))
throw new ArgumentException("图片文件夹路径不能为空", nameof(imageFolder));
if (!Directory.Exists(imageFolder))
throw new DirectoryNotFoundException("图片文件夹不存在: " + imageFolder);
// —— 收集图片文件 ——
// 用不区分大小写的 HashSet 存放允许的图片扩展名
var exts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff", ".wmf", ".emf" };
var files = new List<string>();
// 遍历文件夹下所有文件,挑出图片
foreach (var f in Directory.EnumerateFiles(imageFolder))
{
if (exts.Contains(Path.GetExtension(f)))
files.Add(f);
}
// 一张图片都没有则直接返回失败
if (files.Count == 0) return false;
// 按文件名自然排序(1.jpg < 2.jpg < 10.jpg),保证页码顺序符合直觉
files.Sort(NaturalCompare);
// 确保目标文件所在目录存在,否则创建
string dir = Path.GetDirectoryName(destPath) ?? string.Empty;
if (dir.Length > 0 && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
// —— 步骤1:从内置模板克隆出目标文件 ——
// 模板含完整母版/版式/主题,保证结构合法可被 Office 打开(这是关键,缺这些 Office 会拒绝打开)
byte[] tplBytes = Convert.FromBase64String(EmbeddedTemplateBase64);
// 把模板字节装入只读内存流
using (var tplStream = new MemoryStream(tplBytes, 0, tplBytes.Length, writable: false))
// 以只读方式打开模板文档
using (var tplDoc = PresentationDocument.Open(tplStream, false))
// 准备一个内存流接收克隆结果
using (var outStream = new MemoryStream())
{
// 把模板整体克隆到内存流(深拷贝所有部件)
tplDoc.Clone(outStream);
// 写完后把流指针拨回起点,准备写出
outStream.Position = 0;
// 用独占方式创建目标文件并写入克隆结果(避免句柄延迟释放导致后续打开失败)
using (var outFs = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None))
outStream.WriteTo(outFs);
}
// —— 步骤2:打开目标文件,删除模板原有幻灯片,追加图片幻灯片 ——
// 以可写方式重新打开刚写出的目标文件
using (var doc = PresentationDocument.Open(destPath, true))
{
// 取演示文稿部件(PresentationPart 是 pptx 的核心部件,承载 Presentation)
var presPart = doc.PresentationPart ?? throw new InvalidOperationException("模板缺少 PresentationPart");
// 取幻灯片ID列表(p:sldIdLst),它记录了所有幻灯片的顺序与关系ID
var slideIdList = presPart.Presentation?.SlideIdList
?? throw new InvalidOperationException("模板缺少 SlideIdList");
// —— 删除模板原有的空白幻灯片(模板自带1张空白页,我们不需要它) ——
// 先把现有 SlideId 快照到列表(避免遍历时修改集合)
var oldSlideIds = new List<SlideId>(slideIdList.ChildElements.OfType<SlideId>());
foreach (var sid in oldSlideIds)
{
// 通过关系ID拿到对应的 SlidePart
var rel = presPart.GetPartById(sid.RelationshipId!);
// 从包中删除该幻灯片部件
presPart.DeletePart(rel);
// 从ID列表中移除该条目
sid.Remove();
}
// —— 取母版下的第一个版式,作为新幻灯片的版式引用 ——
// 每张幻灯片必须引用一个版式,否则 Office 打不开
var masterPart = presPart.SlideMasterParts.First();
var layoutPart = masterPart.SlideLayoutParts.First();
// —— 如模板幻灯片尺寸与目标不同,更新之 ——
var slideSize = presPart.Presentation?.GetFirstChild<SlideSize>();
// 读取模板当前尺寸(用 ?.Value 防止 Cx/Cy 为 null)
int curCx = slideSize?.Cx?.Value ?? 0;
int curCy = slideSize?.Cy?.Value ?? 0;
if (slideSize != null && (curCx != slideWidthEmu || curCy != slideHeightEmu))
{
slideSize.Cx = slideWidthEmu; // 设置幻灯片宽度
slideSize.Cy = slideHeightEmu; // 设置幻灯片高度
// 备注页尺寸跟随幻灯片(宽高互换)
var notesSize = presPart.Presentation?.GetFirstChild<NotesSize>();
if (notesSize != null) { notesSize.Cx = slideHeightEmu; notesSize.Cy = slideWidthEmu; }
}
// —— 逐张图片创建幻灯片 ——
uint idx = 0; // 幻灯片ID从256起递增(Office约定SlideId通常≥256)
foreach (var imgPath in files)
{
// 在演示文稿中新建一个幻灯片部件
var slidePart = presPart.AddNewPart<SlidePart>();
// 用图片构造幻灯片内容(见 CreateSlideWithImage)
slidePart.Slide = CreateSlideWithImage(slidePart, imgPath, slideWidthEmu, slideHeightEmu);
// 把该幻灯片关联到版式(建立 slide→layout 关系,关系链完整性的关键)
slidePart.AddPart(layoutPart, "rIdLayout");
// 取该幻灯片在 Presentation 中的关系ID
var rId = presPart.GetIdOfPart(slidePart);
// 在幻灯片ID列表中登记这张幻灯片
slideIdList.Append(new SlideId { Id = 256U + idx, RelationshipId = rId });
idx++;
}
// 保存 Presentation 的修改(把内存中的改动写回包)
presPart.Presentation?.Save();
}
return true;
}
/// <summary>
/// 构造一张含图片的幻灯片:图片等比缩放、居中放置在幻灯片上。
/// </summary>
private static Slide CreateSlideWithImage(SlidePart slidePart, string imgPath, int slideW, int slideH)
{
// —— 1. 读取图片原始像素尺寸,用于等比缩放计算 ——
int imgW, imgH;
GetImageSize(imgPath, out imgW, out imgH);
// —— 2. 计算图片等比缩放后的目标尺寸(EMU),铺满幻灯片但不超出 ——
long targetW, targetH;
if (imgW <= 0 || imgH <= 0)
{
// 读不到尺寸时兜底:直接铺满整页
targetW = slideW; targetH = slideH;
}
else
{
// 取宽高两个方向缩放比的较小值,保证图片完整显示且不变形
double ratio = Math.Min((double)slideW / imgW, (double)slideH / imgH);
targetW = (long)(imgW * ratio); // 缩放后宽度
targetH = (long)(imgH * ratio); // 缩放后高度
}
// 居中偏移:让图片在幻灯片上水平/垂直居中
long offX = (slideW - targetW) / 2;
long offY = (slideH - targetH) / 2;
// —— 3. 把图片文件作为 ImagePart 添加到当前幻灯片 ——
// 注意:ImagePart 必须加到 SlidePart,不能加到 PresentationPart
var imgPart = slidePart.AddImagePart(ImagePartTypeFor(imgPath));
// 以共享读方式打开图片源文件,复制到 ImagePart 的流中
using (var fs = new FileStream(imgPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
fs.CopyTo(imgPart.GetStream(FileMode.Create));
// 取图片部件在本幻灯片中的关系ID,供下面 Blip 引用
string rId = slidePart.GetIdOfPart(imgPart);
// —— 4. 构造 Picture 元素(p:pic),由三部分组成:nvPicPr + blipFill + spPr ——
var pic = new Picture(
// p:nvPicPr 非可视属性:cNvPr(id/名称) + cNvPicPr(锁定纵横比) + nvPr
new NonVisualPictureProperties(
new NonVisualDrawingProperties { Id = 2U, Name = Path.GetFileName(imgPath) },
new NonVisualPictureDrawingProperties(new D.PictureLocks { NoChangeAspect = true }), // 锁定比例,防止变形
new ApplicationNonVisualDrawingProperties()),
// p:blipFill 图片填充:blip 通过 rId 引用上面的图片部件,stretch 表示拉伸填充
new BlipFill(new D.Blip { Embed = rId }, new D.Stretch(new D.FillRectangle())),
// p:spPr 形状属性:位置/尺寸(transform) + 预设几何(矩形)
new ShapeProperties(
new D.Transform2D(new D.Offset { X = offX, Y = offY }, new D.Extents { Cx = targetW, Cy = targetH }),
new D.PresetGeometry(new D.AdjustValueList()) { Preset = D.ShapeTypeValues.Rectangle }));
// —— 5. 构造形状树(p:spTree),把图片放进去 ——
// spTree 必须有 nvGrpSpPr 和 grpSpPr 两个必需子元素
var spTree = new ShapeTree(
// p:nvGrpSpPr 组的非可视属性
new NonVisualGroupShapeProperties(new NonVisualDrawingProperties { Id = 1U, Name = "" }, new NonVisualGroupShapeProperties(), new ApplicationNonVisualDrawingProperties()),
// p:grpSpPr 组的形状属性(变换)
new GroupShapeProperties(new D.Transform2D(new D.Offset { X = 0, Y = 0 }, new D.Extents { Cx = 0, Cy = 0 })));
// 把图片追加到形状树
spTree.Append(pic);
// —— 6. 组装幻灯片 ——
// 幻灯片由 CommonSlideData(含 spTree) 构成
var slide = new Slide(new CommonSlideData(spTree));
// 追加颜色映射覆盖(使用母版默认映射)
slide.Append(new ColorMapOverride(new D.MasterColorMapping()));
return slide;
}
// 标注仅 Windows:System.Drawing.Common 在非 Windows 平台不受支持
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
private static void GetImageSize(string path, out int width, out int height)
{
width = 0; height = 0; // 默认值,读不到尺寸时由调用方兜底
try
{
// 用 GDI+ 读取图片文件,拿到像素宽高
using var img = System.Drawing.Image.FromFile(path);
width = img.Width;
height = img.Height;
}
catch { }
// 异常时保持(0,0),调用方会用"铺满整页"兜底,不会崩溃
}
/// <summary>
/// 根据图片扩展名返回 Open XML 的 PartTypeInfo(3.x 用 PartTypeInfo 取代旧版 ImagePartType 枚举)。
/// </summary>
private static PartTypeInfo ImagePartTypeFor(string imgPath)
{
// 取扩展名并转小写,按 switch 映射到对应的图片类型枚举
return Path.GetExtension(imgPath).ToLowerInvariant() switch
{
".jpg" or ".jpeg" => ImagePartType.Jpeg,
".png" => ImagePartType.Png,
".bmp" => ImagePartType.Bmp,
".gif" => ImagePartType.Gif,
".tif" or ".tiff" => ImagePartType.Tiff,
".wmf" => ImagePartType.Wmf,
".emf" => ImagePartType.Emf,
_ => ImagePartType.Jpeg, // 未知扩展名兜底为 JPEG
};
}
/// <summary>自然排序比较器:直接转调 Windows API,实现 1<2<10 的数字感知排序。</summary>
private static int NaturalCompare(string a, string b) => StrCmpLogicalW(a, b);
// 调用 Windows shell 的 StrCmpLogicalW:按"自然顺序"比较字符串(数字按数值而非字符序比较)
[System.Runtime.InteropServices.DllImport("shlwapi.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private static extern int StrCmpLogicalW(string a, string b);
/// <summary>
/// 读取 pptx 的幻灯片数量(便于调用后校验生成结果)。
/// </summary>
public static int GetSlideCount(string path)
{
// 文件不存在直接返回0
if (!File.Exists(path)) return 0;
// 以只读方式打开 pptx
using var doc = PresentationDocument.Open(path, false);
var pres = doc.PresentationPart?.Presentation;
// 返回幻灯片ID列表的子元素数量(即幻灯片张数)
return pres?.SlideIdList?.ChildElements.Count ?? 0;
}
}
}

浙公网安备 33010602011771号