ARTICLE · 1299205
不装 Office 也能生成 PPT:Wisdom.Ppt 使用指南
「纯 .NET 操作 Office」系列 · PowerPoint 篇
写在前面
服务端生成 PowerPoint,比 Excel / Word 更让人头疼。Interop 要求机器上装 Office,服务端基本不能用;Open XML SDK 虽然纯净,但要手写冗长的 PresentationML,还得时刻记住坐标是 EMU、位移写在 a:off、尺寸写在 a:ext;商用库则绕不开授权。
Wisdom.Ppt 的定位很明确:用模型描述幻灯片,写出器负责 PresentationML。你用 OfficePresentation 摆好版式、形状、文本、动画,PptxWriter 把它落成 .pptx;反过来 PptxReader 把 .pptx 读回同一模型。全程由 System.IO.Compression.ZipArchive + LINQ to XML 直接处理,不依赖 Office、WPS 或任何第三方库,唯一依赖是自己家的数据模型包 Wisdom.Office.Core。
dotnet add package Wisdom.Ppt
支持 net461 / netstandard2.0 / net7.0 / net8.0 / net9.0 / net10.0 六份目标框架,同一份源码编译,老 .NET Framework 项目和 Unity 都能引。
一、单位与坐标系
这是最容易搞错的地方,先讲清楚。所有几何量以磅(point)为单位,写出时按 1 磅 = 12700 EMU 换算成 PresentationML 的 a:off / a:ext,坐标原点为幻灯片左上角,X 向右、Y 向下。你不需要碰 EMU,但需要知道画布多大。
OfficePresentation.Widescreen() | |||
OfficePresentation.Standard() |
摆一个 left=96, top=60, width=768, height=80 的文本框,就是距左上角 96 磅、宽 768 磅——在 960 宽的画布上左右各留 96 磅边距。两套尺寸下占位符按各自宽度等比推算,同一份代码换 Standard() 会自动收缩。
二、构建演示文稿
AddSlide(SlideLayoutKind) 会按版式生成占位符形状,每个占位符内预置一个空段落,取出后直接 Paragraphs[0].AddText(...) 填内容。
using Wisdom.Ppt;
using Wisdom.Office.Core;
OfficePresentation presentation = OfficePresentation.Widescreen();
presentation.Metadata.Title = "季度经营回顾";
Slide cover = presentation.AddSlide(SlideLayoutKind.Title);
TextBoxShape coverTitle = (TextBoxShape)cover.Shapes[0]; // Title 版式只有标题占位符
coverTitle.Paragraphs[0].AddText("2026 年第三季度经营回顾");
cover.NotesText = "开场 2 分钟,说明本次回顾的三个议题。";
Slide bullets = presentation.AddSlide(SlideLayoutKind.TitleAndContent);
((TextBoxShape)bullets.Shapes[0]).Paragraphs[0].AddText("本季度要点");
TextBoxShape body = FindContent(bullets);
body.Paragraphs[0].AddText("收入 1,280 万元,环比增长 12%");
body.Paragraphs[0].ListMarker = "•"; // 项目符号
body.Paragraphs[0].LeftIndent = 18; // 缩进(磅)
foreach (string item innew[] { "成本控制在预算内,毛利率 41.6%", "华东区贡献了 46% 的增量收入" })
{
Paragraph paragraph = body.AddParagraph(string.Empty);
paragraph.AddText(item);
paragraph.ListMarker = "•";
paragraph.LeftIndent = 18;
}
bullets.Transition = SlideTransitionKind.Fade; // 切换效果
bullets.TransitionDuration = 0.7; // 秒
PptxWriter.Write(presentation, "review.pptx"); // 亦支持 Stream,或 byte[] Write(presentation)
SlideTransitionKind 支持 None、Fade、Push、Wipe、Split。生成的包通过内容类型、关系目标与 XML 合法性校验,可被 PowerPoint 2010+、WPS 演示直接打开。
三、七种版式与占位符
AddSlide 不带参数等价于 Blank,全部形状自己定位。占位符的字体不在模型里写死,由写出器套用 DefaultTitleFormat / DefaultTextFormat。
SlideLayoutKind | |
|---|---|
Title | Title |
TitleAndContent | TitleContent |
SectionHeader | Title |
TwoContent | TitleContent |
TitleAndPicture | TitlePicture |
TitleAndTable | TitleTable |
Blank |
四、文本、图形、图片与表格
四种形状对应四种构造方式:
TextBoxShape | slide.AddTextBox(left, top, width, height, text) | ParagraphsVerticalAlignment、四边内边距、ShrinkToFit |
AutoShape | slide.AddShape(AutoShapeKind, ...) | FillColorOutline、CornerRadius |
PictureShape | slide.AddImage(new OfficeImage(bytes), ...) | AlternativeText 作替代文字 |
TableShape | slide.AddTable(left, top, width, height) | Table,复用 Word 侧的行列与合并单元格语义 |
Slide shapes = presentation.AddSlide(SlideLayoutKind.Blank);
AutoShape banner = shapes.AddShape(AutoShapeKind.RoundedRectangle, 96, 60, 768, 80, "达成率 96%");
banner.FillColor = new OfficeColor(0x00, 0xB0, 0x50);
banner.Outline = new BorderLine(BorderStyle.Single, 1, new OfficeColor(0x1F, 0x3A, 0x5F));
banner.CornerRadius = 0.25;
banner.Paragraphs[0].Alignment = ParagraphAlignment.Center;
shapes.AddImage(OfficeImage.FromFile("chart.png"), 96, 180, 320, 200);
shapes.AddTextBox(440, 205, 424, 120, "绿色圆角矩形为达标线以上的达成率");
TableShape table = shapes.AddTable(96, 330, 768, 150);
table.HeaderColor = new OfficeColor(0x1F, 0x3A, 0x5F);
table.Table.AddRow("区域", "收入(万元)", "占比");
table.Table.AddRow("华东", "588", "46%");
table.Table.AddRow("华南", "384", "30%");
AutoShapeKind 支持 Rectangle、RoundedRectangle、Ellipse、Triangle、Diamond、Pentagon、Hexagon、RightArrow、Chevron、Line 十种,逐个映射到 PresentationML 的 prstGeom。
五、入场动画与自动换页
每个 SlideShape 都能挂一个入场动画;写出器据此生成 p:timing 主序列(PowerPoint 原生动画节点)。三个属性即够:
Slide animated = presentation.AddSlide(SlideLayoutKind.Blank);
TextBoxShape headline = animated.AddTextBox(96, 80, 768, 70, "季度收入 1,280 万元");
headline.Animation = SlideAnimationKind.FlyIn; // None / Fade / FlyIn / Wipe / Zoom / Split
headline.AnimationDurationSeconds = 0.8; // 0 表示按类别取默认时长
headline.AnimationDelaySeconds = 0.5; // 自动触发时的延迟
TextBoxShape subhead = animated.AddTextBox(96, 160, 768, 50, "环比增长 12%");
subhead.Animation = SlideAnimationKind.Wipe;
animated.AdvanceAfterSeconds = 3.0; // p:transition 的 advTm,3 秒后自动换页
注意:只有入场类别被建模,强调、退出、路径与媒体动画不处理。
六、主题深改与 .potx 模板
PresentationTheme 覆盖 theme1.xml 的六个色板阶段与主/次字体,null 字段保持内置默认(强调色仍走 AccentColors),母版与版式经 clrMap 自动跟随:
presentation.Theme = new PresentationTheme
{
Name = "品牌主题",
Text = new OfficeColor(0x1A, 0x1A, 0x2E), // dk1
Background = new OfficeColor(0xFA, 0xFA, 0xF5), // lt1
TextSecondary = new OfficeColor(0x2F, 0x4D, 0x6B), // dk2
Hyperlink = new OfficeColor(0xC0, 0x39, 0x2B),
FontMajor = "微软雅黑", // majorFont/latin
FontMinor = "Consolas"// minorFont/latin
};
模板格式 .potx 与 .pptx 包结构完全一致,仅 /ppt/presentation.xml 的内容类型不同:
PptxWriter.WriteTemplate(presentation, "brand.potx"); // 文件 / 流 / 字节重载均有
七、读取 PPTX
OfficePresentation loaded = PptxReader.Load("review.pptx"); // 亦支持 Stream / byte[]
Console.WriteLine($"{loaded.Slides.Count} 页,{loaded.SlideWidth} x {loaded.SlideHeight}");
foreach (Slide slide in loaded.Slides)
{
Console.WriteLine(slide.GetTitle()); // 标题占位符文本
foreach (SlideShape shape in slide.Shapes)
{
TextBoxShape box = shape as TextBoxShape;
if (box != null) Console.WriteLine(box.GetText());
TableShape tbl = shape as TableShape;
if (tbl != null) Console.WriteLine($"{tbl.Table.Rows.Count} 行");
}
}
string plain = PptxReader.ExtractText("review.pptx"); // 只要纯文本时用这个
读取覆盖页数与幻灯片尺寸、版式类别、占位符类型、形状几何与填充、段落与行内格式(粗体、斜体、下划线、删除线、上下标、字号、字体、颜色)、项目符号、表格(含合并单元格)、图片字节与替代文字、演讲者备注、切换效果、入场动画、advTm 自动换页、PresentationTheme 深改字段(.potx 同样可加载)以及 core.xml 里的文档元数据。
八、往返保真:诚实说明差异
.pptx 里没有可直接对照的"版式类别"字段,读回时版式是按形状推断的(PptxReader.InferLayoutKind):先看图片形状或 pic 占位符,再看表格形状与 tbl / chart 占位符,都没有时按标题与内容占位符的个数归入 TitleAndContent / TwoContent / Title / Blank。因此往返后版式名可能被"修正",这里不粉饰,实测几条差异讲清楚:
AddSlide(TitleAndTable)生成的表格占位符按obj类型写出,回读变成TitleAndContent;反过来,AddSlide(TitleAndContent)上手工AddTable(...)得到的真实表格形状,回读会被推断成TitleAndTable。表头占位形状回读成 TextBoxShape。表格的列定义 Table.Columns写出时不入库(实测写出为 2 行 0 列),回读按每行单元格数补齐(2 行 2 列)。
好消息是几何与内容层面基本按原值读回:占位符几何(实测 Left 60 原样读回)、图片字节与 alt 文本、备注、切换类型与时长(Push / 2.25 秒)、背景色(#F2F7FF)与自选图形(RoundedRectangle 填充 #00B050、CornerRadius 默认 0.15)都能回读。一句话:内容和坐标可靠,版式名以推断为准。
九、经 Wisdom.Office 门面使用
Wisdom.Ppt 只做 PPTX 读写;要转 PDF / Word,请改引 Wisdom.Office 门面包(本包不依赖 Wisdom.Pdf 与 Wisdom.Word)。
using Wisdom.Office;
using Wisdom.Office.Core;
OfficeFile file = OfficeFile.Load("review.pptx"); // 按文件头识别为 Pptx
OfficeDocument asWord = file.AsDocument(); // 每页转成一节,标题降级为段落
file.Save("review.pdf"); // 需要门面包
// 显式转换入口
OfficeConverter.PresentationToPdf(presentation, "review.pdf");
OfficeConverter.DocumentToPptx(document, "from-word.pptx"); // byte[] 重载亦有
file.AsPresentation() 还能把 Word 文档按一级标题反向拆成幻灯片。
已知边界
不支持读写旧版 .ppt(PowerPoint 97-2003 二进制)。不处理图表、SmartArt 与嵌入 OLE 对象:读取时忽略这些部件,写出时不生成。 动画只支持入场类别( p:timing主序列);强调、退出、路径与媒体动画不建模,读取时只识别入场节点。母版 / 版式 / 主题底稿由本库固定生成一套 slideMaster/slideLayout/theme,深改经PresentationTheme显式覆盖;.potx可读写,但不继承第三方母版的精确占位符坐标,占位符位置仍按幻灯片尺寸等比推算。文本换行、自动字号、形状阴影与 3D 效果由 PowerPoint / WPS 运行时决定,本包只输出对应属性,不做排版模拟。 图片仅支持 PNG / JPEG。
小结
Wisdom.Ppt 的思路和系列里其它包一致:把 PresentationML 当作一个可以直接读写的 ZIP + XML 包来处理,你只面对 OfficePresentation 模型。它覆盖了服务端生成 PPT 的核心场景——版式、四种形状、十种自选图形、项目符号、备注、切换、入场动画、自动换页、主题深改与 .potx 模板,代价是明确放弃旧 .ppt、图表 / SmartArt / OLE、非入场动画和排版模拟。
如果目标是"用代码描述一份结构清晰的演示文稿并写出 PPTX",而不是"100% 还原设计师在 PowerPoint 里排好的复杂母版",这个包够用,且没有许可证和体积负担。需要跨 PDF / Word 互转的,再看同系列的 Wisdom.Office 门面包。















































































