This article is part of a series on using WPF to create a PowerPoint-like application. This post explains how to parse the text outline effect in PPT files and render it in a WPF application, achieving pixel-level consistency.
Background Knowledge
Before you start, you should have basic knowledge of PPT parsing. If you are unfamiliar with PPT parsing, please refer to C# dotnet - Using OpenXml to Parse PPT Files.
In PPT, you can set an outline effect on certain characters of text. In OpenXML terms, this is not an effect but rather a border property. In PPT, you can add an Outline border property to text to create a text outline.
Effect
Before we begin, let’s take a look at the result.

Parsing
Before parsing, we first read the document. The code is as follows. The full code and test file can be obtained at the end of this article.
var file = new FileInfo("Test.pptx");
using var presentationDocument = PresentationDocument.Open(file.FullName, false);
var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;
The following code focuses on the core logic, ignoring many parameter checks for the sake of simplicity based on the Test.pptx document. In a real project, you should perform the necessary parameter checks yourself.
In this test document, the first slide contains only one element – the text with the outline effect. The code to obtain it is:
var shape = slide.CommonSlideData!.ShapeTree!.GetFirstChild<Shape>()!;
The OpenXML content of this Shape is approximately as follows:
<p:sp>
<p:spPr>
<a:prstGeom prst="rect">
</a:prstGeom>
<a:noFill />
</p:spPr>
<p:txBody>
<a:bodyPr wrap="square" rtlCol="0">
<a:spAutoFit />
</a:bodyPr>
<a:lstStyle />
<a:p>
<a:r>
<a:rPr lang="zh-CN" altLang="en-US" sz="10000">
<a:ln w="9525">
<a:solidFill>
<a:srgbClr val="00FF00" />
</a:solidFill>
</a:ln>
</a:rPr>
<a:t>一行文本</a:t>
</a:r>
<a:endParaRPr lang="en-US" sz="10000" dirty="0" />
</a:p>
</p:txBody>
</p:sp>
In PPT, a text box is also a shape, by default a rectangle.
var shapeProperties = shape.ShapeProperties!;
var presetGeometry = shapeProperties.GetFirstChild<PresetGeometry>()!;
// This is a text box
Debug.Assert(presetGeometry.Preset?.Value == ShapeTypeValues.Rectangle);
Debug.Assert(shapeProperties.GetFirstChild<NoFill>() is not null);
The above code is just to show how to retrieve a shape. In your own business logic, you need to add the appropriate checks.
To obtain the text in the text box, use the following code:
var textBody = shape.TextBody!;
Debug.Assert(textBody != null);
A text body contains multiple paragraphs. Within a paragraph, text can have different styles, for example, one part can be bold while another is not. Text with the same style is placed in one TextRun. Text with different styles is placed in separate TextRun elements.
Therefore, parsing requires first iterating over paragraphs and then over TextRun elements.
foreach (var paragraph in textBody.Elements<DocumentFormat.OpenXml.Drawing.Paragraph>())
{
// This text paragraph has no properties; for simplicity, code is omitted
//if (paragraph.ParagraphProperties != null)
foreach (var run in paragraph.Elements<DocumentFormat.OpenXml.Drawing.Run>())
{
}
}
Obtain the properties of a TextRun as follows:
var runProperties = run.RunProperties!;
From these properties, you can get the font size of the current text:
var fontSize = new PoundHundredfold(runProperties.FontSize!.Value).ToPound();
Next, the core of this article: obtaining the Outline property:
var outline = runProperties.Outline!;
The corresponding OpenXML code is:
<a:ln w="9525"> <a:solidFill>
<a:srgbClr val="00FF00" />
</a:solidFill>
</a:ln>
The main attributes we care about are the width and color. They can be obtained as follows:
var outlineWidth = new Emu(outline.Width!.Value);
And the color:
var solidFill = outline.GetFirstChild<SolidFill>()!;
var rgbColorModelHex = solidFill.GetFirstChild<RgbColorModelHex>()!;
var colorText = rgbColorModelHex.Val!;
Using the method from Win10 UWP Color Conversion, you can convert colorText to a SolidColorBrush object.
Then, retrieving the text content is roughly done:
// Default font foreground color is black
var text = run.Text!.Text;
Now, we move on to the rendering.
Rendering
As described in the WPF Text Outline blog, first use FormattedText to build a Geometry object, then render using the Geometry object.
The code is:
var formattedText = new FormattedText(text, CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface
(
// Default is SimSun
new FontFamily("SimSun"),
FontStyles.Normal,
FontWeights.Normal,
FontStretches.Normal
),
// In WPF, EM units are used, roughly equivalent to pixel units
fontSize.ToPixel().Value,
Brushes.Black, 96);
The code to build the Geometry object from FormattedText is:
var geometry = formattedText.BuildGeometry(new ());
Then, use System.Windows.Shapes.Path to render the Geometry onto the UI.
var path = new System.Windows.Shapes.Path
{
Data = geometry,
Fill = Brushes.Black,
Stroke = BrushCreator.CreateSolidColorBrush(colorText),
StrokeThickness = outlineWidth.ToPixel().Value,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
Root.Children.Add(path);
With the above code, you can draw an interface identical to the PPT.
Code
All the code and the test file in this article are available on github and gitee. Feel free to visit.
You can obtain the source code of this article using the following method: first create an empty folder, then use the command line to cd into that folder, and enter the following command in the command line to get the code:
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 71af5b0e47493ff7f5f43be33583265805da9d84
The above uses the gitee source. If gitee is inaccessible, replace it with the github source:
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
After obtaining the code, enter the Pptx folder.
References
- WPF Text Outline
- For more information, see Office - Using OpenXML SDK to Parse Documents Blog Directory
The blog garden blog is only a backup; the blog will not be updated after publication. If you want to see the latest blog, please go to https://blog.lindexi.com/.
Knowledge Sharing License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. You are free to share, adapt, and republish with attribution, but you must retain the author's name (Lin Dexi) and link (http://blog.csdn.net/lindexi_gd), not use for commercial purposes, and distribute any modifications under the same license. If you have any questions, please contact me.