Outline text from a Windows 10 Store XAML App using SharpDX

 

I recently needed to draw text outlines on one of my projects from a Windows 10 Store app. This is not directly supported by XAML so I had to use some SharpDX DirectX code to retrieve the font outlines and then draw as a Path object. The result can be seen here.

 

image

 

I packaged up the solution into an easy to use User Control so the XAML used for the above was easy:

 

<local:OutlineTextBlock Text="Hello World!" FontSize="100" FontFamily="Ravie" StrokeThickness="2" VerticalAlignment="Center" HorizontalAlignment="Center">
<local:OutlineTextBlock.Stroke>
<LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0">
<GradientStop Color="Red" Offset="0.3"/>
<GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>
</local:OutlineTextBlock.Stroke>
</local:OutlineTextBlock>

 

You can just use the above code to draw text outlines in your project, but if you want to understand how this works, read on.

Fetching the text outlines was quite tricky and involved implementing a DirectX custom text renderer which renders the text into a custom Geometry Sink which in turn converts the font outlines into a PathGeometry which can be used to create a XAML Path object. I found this post which was very nearly what I needed but it still took a lot of work to turn it into the finished result. https://stackoverflow.com/questions/7563798/how-to-implement-the-outline-effect-on-text-with-windowsapicodepack 

 

Here is the code:

    public Sink SimplifiedGeometrySink { get; set; }
public OutlineTextRenderer(RenderTarget surface, SharpDX.Direct2D1.Brush brush)
{
_factory = surface.Factory;
_surface = surface;
_brush = brush;
}

    public Result DrawGlyphRun(object clientDrawingContext, float baselineOriginX, float baselineOriginY, MeasuringMode measuringMode, SharpDX.DirectWrite.GlyphRun glyphRun, SharpDX.DirectWrite.GlyphRunDescription glyphRunDescription, ComObject clientDrawingEffect)
{
using (SharpDX.Direct2D1.PathGeometry path = new SharpDX.Direct2D1.PathGeometry(_factory))
{
using (GeometrySink sink = path.Open())
{
glyphRun.FontFace.GetGlyphRunOutline(glyphRun.FontSize, glyphRun.Indices, glyphRun.Advances, glyphRun.Offsets, glyphRun.IsSideways, (glyphRun.BidiLevel % 2) > 0, sink);
sink.Close();
}

            this.SimplifiedGeometrySink = new Sink();
path.Simplify(GeometrySimplificationOption.CubicsAndLines, (SharpDX.Direct2D1.SimplifiedGeometrySink)this.SimplifiedGeometrySink);
}

        return new Result();
}

    public Result DrawInlineObject(object clientDrawingContext, float originX, float originY, SharpDX.DirectWrite.InlineObject inlineObject, bool isSideways, bool isRightToLeft, ComObject clientDrawingEffect)
{
return new Result();
}

    public Result DrawStrikethrough(object clientDrawingContext, float baselineOriginX, float baselineOriginY, ref SharpDX.DirectWrite.Strikethrough strikethrough, ComObject clientDrawingEffect)
{
return new Result();
}

    public Result DrawUnderline(object clientDrawingContext, float baselineOriginX, float baselineOriginY, ref SharpDX.DirectWrite.Underline underline, ComObject clientDrawingEffect)
{
SharpDX.Direct2D1.DeviceContext d2dContext = clientDrawingContext as SharpDX.Direct2D1.DeviceContext;
RawRectangleF rect = new RawRectangleF(0, underline.Offset, underline.Width, underline.Offset + underline.Thickness);
SharpDX.Direct2D1.RectangleGeometry geom = new SharpDX.Direct2D1.RectangleGeometry(d2dContext.Factory, rect);
geom.Simplify(GeometrySimplificationOption.Lines, this.SimplifiedGeometrySink);
return new Result();
}

    public float GetPixelsPerDip(object clientDrawingContext)
{
return 0;
}

    public bool IsPixelSnappingDisabled(object clientDrawingContext)
{
return false;
}

    RawMatrix3x2 PixelSnapping.GetCurrentTransform(object clientDrawingContext)
{
return new RawMatrix3x2();
}
}

public class Sink : SharpDX.Direct2D1.SimplifiedGeometrySink
{
Windows.UI.Xaml.Media.PathFigureCollection figures = new Windows.UI.Xaml.Media.PathFigureCollection();
Windows.UI.Xaml.Media.PathFigure currentFigure = null;
public Windows.UI.Xaml.Media.PathFigureCollection PathFigureCollection { get { return figures; } }
public IDisposable Shadow
{
get
{
return null;
}

        set
{
}
}

    public void BeginFigure(RawVector2 startPoint, FigureBegin figureBegin)
{
currentFigure = new Windows.UI.Xaml.Media.PathFigure { StartPoint = new Windows.Foundation.Point(startPoint.X, startPoint.Y) };
}

    public void AddBeziers(SharpDX.Direct2D1.BezierSegment[] beziers)
{
foreach (SharpDX.Direct2D1.BezierSegment bez in beziers)
{
currentFigure.Segments.Add(new Windows.UI.Xaml.Media.BezierSegment
{
Point1 = new Windows.Foundation.Point(bez.Point1.X, bez.Point1.Y),
Point2 = new Windows.Foundation.Point(bez.Point2.X, bez.Point2.Y),
Point3 = new Windows.Foundation.Point(bez.Point3.X, bez.Point3.Y)
});
}
}

    public void AddLines(RawVector2[] pointsRef)
{
foreach (RawVector2 vect in pointsRef)
{
currentFigure.Segments.Add(new Windows.UI.Xaml.Media.LineSegment { Point = new Windows.Foundation.Point(vect.X, vect.Y) });
}
}

    public void EndFigure(FigureEnd figureEnd)
{
currentFigure.IsClosed = figureEnd == FigureEnd.Closed;
figures.Add(currentFigure);
}

    public void Close()
{
}

    public void Dispose()
{
figures = new Windows.UI.Xaml.Media.PathFigureCollection();
currentFigure = null;
}

    public void SetFillMode(SharpDX.Direct2D1.FillMode fillMode)
{
}

    public void SetSegmentFlags(SharpDX.Direct2D1.PathSegment vertexFlags)
{
}

I have included a demo project with all the code. You are free to use it in your code, even for commercial use.

 

https://theuxblog20160710035436.azurewebsites.net/OutlineTextDemo.zip

 

Enjoy!

Paul Tallett, UX Global Practice, Microsoft UK

Disclaimer: The information on this site is provided "AS IS" with no warranties, confers no rights, and is not supported by the authors or Microsoft Corporation. Use of included script samples are subject to the terms specified in the Terms of Use.