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
Protected Overridable Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
End If
End If
Me.disposedValue = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
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
Protected Overridable Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
End If
End If
Me.disposedValue = True
End Sub
(ByVal disposing As Boolean) above has code to free unmanaged resources.
Public Sub Dispose() Implements IDisposable.Dispose
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))
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