Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Analyzing Text With DirectWrite in .NET Using SharpDX

0.00/5 (No votes)
20 Nov 2014 1  
Analyzing Text With DirectWrite in .NET Using SharpDX

Introduction

Computing precise detailed calculations about complex scripts falls short when using GDI and Uniscribe. If one wants to make text calculations that are going to be used for crystal clear rendering to printed formats or high resolution displays and such, then DirectWrite and the TextAnalyzer classes are where one should look. The SharpDX and specifically SharpDX.Direct2D1 fortunately wraps all of the interfaces to provide an easy to utilize .NET component that makes implementing functionality from there relatively easy.

Background

Basic familiarity with DirectX, DirectWrite, implementing callback interfaces and Unicode scripts should be enough.

Using the Code

The code gives an example of calculating the offsets and placement of combining symbols in a script. The process is relatively straightforward though getting the ScriptAnalysis requires some large but simple to implement interfaces that wrap the source string and an assumption is made that only a single analysis will be made from it in the sink class as it should otherwise maintain a dictionary of position and lengths.

    Structure CharPosInfo
        Public Index As Integer
        Public Width As Single
        Public PriorWidth As Single
        Public X As Single
        Public Y As Single
    End Structure
    Const ERROR_INSUFFICIENT_BUFFER As Integer = 122
    Class TextSource
        Implements SharpDX.DirectWrite.TextAnalysisSource
        Public Sub New(Str As String, Factory As SharpDX.DirectWrite.Factory)
            _Str = Str
            _Factory = Factory
        End Sub
        Dim _Str As String
        Dim _Factory As SharpDX.DirectWrite.Factory
        Public Function GetLocaleName(textPosition As Integer, _
        ByRef textLength As Integer) As _
        String Implements SharpDX.DirectWrite.TextAnalysisSource.GetLocaleName
            Return Threading.Thread.CurrentThread.CurrentCulture.Name
        End Function
        Public Function GetNumberSubstitution(textPosition As Integer, _
        ByRef textLength As Integer) As SharpDX.DirectWrite.NumberSubstitution _
        Implements SharpDX.DirectWrite.TextAnalysisSource.GetNumberSubstitution
            Return New SharpDX.DirectWrite.NumberSubstitution(_Factory, _
            SharpDX.DirectWrite.NumberSubstitutionMethod.None, Nothing, True)
        End Function
        Public Function GetTextAtPosition(textPosition As Integer) _
        As String Implements SharpDX.DirectWrite.TextAnalysisSource.GetTextAtPosition
            Return _Str.Substring(textPosition)
        End Function
        Public Function GetTextBeforePosition(textPosition As Integer) _
        As String Implements SharpDX.DirectWrite.TextAnalysisSource.GetTextBeforePosition
            Return _Str.Substring(0, textPosition - 1)
        End Function
        Public ReadOnly Property ReadingDirection As _
        SharpDX.DirectWrite.ReadingDirection _
        Implements SharpDX.DirectWrite.TextAnalysisSource.ReadingDirection
            Get
                Return SharpDX.DirectWrite.ReadingDirection.RightToLeft
            End Get
        End Property
        Public Property Shadow As IDisposable Implements SharpDX.ICallbackable.Shadow
#Region "IDisposable Support"
        Private disposedValue As Boolean ' To detect redundant calls

        ' IDisposable
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not Me.disposedValue Then
                If disposing Then
                    ' TODO: dispose managed state (managed objects).
                End If

                ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
                ' TODO: set large fields to null.
            End If
            Me.disposedValue = True
        End Sub

        ' TODO: override Finalize() only if 
        ' Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
        'Protected Overrides Sub Finalize()
        '    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        '    Dispose(False)
        '    MyBase.Finalize()
        'End Sub

        ' This code added by Visual Basic to correctly implement the disposable pattern.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code.  Put cleanup code in Dispose(disposing As Boolean) above.
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
#End Region
    End Class
    Class TextSink
        Implements SharpDX.DirectWrite.TextAnalysisSink
        Public Sub SetBidiLevel(textPosition As Integer, textLength As Integer, _
        explicitLevel As Byte, resolvedLevel As Byte) _
        Implements SharpDX.DirectWrite.TextAnalysisSink.SetBidiLevel
            _explicitLevel = explicitLevel
            _resolvedLevel = resolvedLevel
        End Sub
        Public Sub SetLineBreakpoints(textPosition As Integer, _
        textLength As Integer, lineBreakpoints() As SharpDX.DirectWrite.LineBreakpoint) _
        Implements SharpDX.DirectWrite.TextAnalysisSink.SetLineBreakpoints
            _lineBreakpoints = lineBreakpoints
        End Sub
        Public Sub SetNumberSubstitution(textPosition As Integer, _
        textLength As Integer, numberSubstitution As SharpDX.DirectWrite.NumberSubstitution) _
        Implements SharpDX.DirectWrite.TextAnalysisSink.SetNumberSubstitution
            _numberSubstitution = numberSubstitution
        End Sub
        Public Sub SetScriptAnalysis(textPosition As Integer, _
        textLength As Integer, scriptAnalysis As SharpDX.DirectWrite.ScriptAnalysis) _
        Implements SharpDX.DirectWrite.TextAnalysisSink.SetScriptAnalysis
            _scriptAnalysis = scriptAnalysis
        End Sub
        Public _scriptAnalysis As SharpDX.DirectWrite.ScriptAnalysis
        Public _numberSubstitution As SharpDX.DirectWrite.NumberSubstitution
        Public _lineBreakpoints() As SharpDX.DirectWrite.LineBreakpoint
        Public _explicitLevel As Byte
        Public _resolvedLevel As Byte
        Public Property Shadow As IDisposable Implements SharpDX.ICallbackable.Shadow
#Region "IDisposable Support"
        Private disposedValue As Boolean ' To detect redundant calls

        ' IDisposable
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not Me.disposedValue Then
                If disposing Then
                    ' TODO: dispose managed state (managed objects).
                End If

                ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
                ' TODO: set large fields to null.
            End If
            Me.disposedValue = True
        End Sub

        ' TODO: override Finalize() only if Dispose_
        (ByVal disposing As Boolean) above has code to free unmanaged resources.
        'Protected Overrides Sub Finalize()
        '    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        '    Dispose(False)
        '    MyBase.Finalize()
        'End Sub

        ' This code added by Visual Basic to correctly implement the disposable pattern.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code.  Put cleanup code in Dispose(disposing As Boolean) above.
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
#End Region
    End Class
    Public Shared Function GetWordDiacriticPositionsDWrite_
    (Str As String, useFont As Font) As CharPosInfo()
        Dim Factory As New SharpDX.DirectWrite.Factory()
        Dim Analyze As New SharpDX.DirectWrite.TextAnalyzer(Factory)
        Dim FontFace As New SharpDX.DirectWrite.FontFace_
        (Factory.GdiInterop.FromSystemDrawingFont(useFont))
        Dim Analysis As New SharpDX.DirectWrite.ScriptAnalysis
        Dim Sink As New TextSink
        Dim Source As New TextSource(Str, Factory)
        Analyze.AnalyzeScript(Source, 0, Str.Length, Sink)
        Analysis = Sink._scriptAnalysis
        Dim GlyphCount As Integer = Str.Length * 3 \ 2 + 16
        Dim ClusterMap(Str.Length - 1) As Short
        Dim TextProps(Str.Length - 1) As SharpDX.DirectWrite.ShapingTextProperties
        Dim GlyphIndices(GlyphCount - 1) As Short
        Dim GlyphProps(GlyphCount - 1) As SharpDX.DirectWrite.ShapingGlyphProperties
        Dim ActualGlyphCount As Integer
        Do
            Try
                Analyze.GetGlyphs(Str, Str.Length, FontFace, False, True, _
                Analysis, Nothing, Nothing, Nothing, Nothing, GlyphCount, _
                ClusterMap, TextProps, GlyphIndices, GlyphProps, ActualGlyphCount)
                Exit Do
            Catch ex As SharpDX.SharpDXException
                If ex.ResultCode = SharpDX.Result.GetResultFromWin32Error(ERROR_INSUFFICIENT_BUFFER) Then
                    GlyphCount *= 2
                    ReDim GlyphIndices(GlyphCount - 1)
                    ReDim GlyphProps(GlyphCount - 1)
                End If
            End Try
        Loop While True
        ReDim Preserve ClusterMap(ActualGlyphCount - 1)
        ReDim Preserve TextProps(ActualGlyphCount - 1)
        ReDim Preserve GlyphIndices(ActualGlyphCount - 1)
        ReDim Preserve GlyphProps(ActualGlyphCount - 1)
        Dim GlyphAdvances(ActualGlyphCount - 1) As Single
        Dim GlyphOffsets(ActualGlyphCount - 1) As SharpDX.DirectWrite.GlyphOffset
        Analyze.GetGlyphPlacements(Str, ClusterMap, TextProps, Str.Length, _
        GlyphIndices, GlyphProps, ActualGlyphCount, FontFace, useFont.Size, _
        False, True, Analysis, Nothing, Nothing, Nothing, GlyphAdvances, GlyphOffsets)
        Dim CharPosInfos As New List(Of CharPosInfo)
        Dim LastPriorWidth As Single = 0
        Dim RunStart As Integer = 0
        For CharCount = 0 To ClusterMap.Length - 1
            Dim PriorWidth As Single = 0
            Dim RunCount As Integer = 0
            For ResCount As Integer = ClusterMap(CharCount) To _
            If(CharCount = ClusterMap.Length - 1, _
            ActualGlyphCount - 1, ClusterMap(CharCount + 1))
                'fDiacritic or fZeroWidth
                If GlyphProps(ResCount).IsDiacritic Or _
                GlyphProps(ResCount).IsZeroWidthSpace Then
                    CharPosInfos.Add(New CharPosInfo With _
                    {.Index = RunStart + RunCount, .PriorWidth = LastPriorWidth, _
                    .Width = GlyphAdvances(ClusterMap(RunStart)), _
                    .X = GlyphOffsets(ResCount).AdvanceOffset, _
                    .Y = GlyphOffsets(ResCount).AscenderOffset})
                End If
                If CharCount = ClusterMap.Length - 1 _
                OrElse ClusterMap(CharCount) <> ClusterMap(CharCount + 1) Then
                    PriorWidth += GlyphAdvances(ResCount)
                    RunCount += 1
                End If
            Next
            LastPriorWidth += PriorWidth
            If CharCount = ClusterMap.Length - 1 OrElse _
            ClusterMap(CharCount) <> ClusterMap(CharCount + 1) Then
                RunStart = CharCount + 1
            End If
        Next
        Return CharPosInfos.ToArray()
    End Function

Take note that the Try-Catch pattern is used in SharpDX and the first call must be made in a loop as GetGlyphs may require larger arrays and a doubling strategy is used to address it.

Points of Interest

It is relatively easy and straight forward thanks to the SharpDX library. If this library cannot be used, then the easiest wrapper would be in Managed C++ while native C++ makes it easy to use but the code is long and unwieldy.

History

  • Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here