Click here to Skip to main content
15,896,527 members
Articles / Multimedia / GDI+

Text on Path with VB.NET

Rate me:
Please Sign up or sign in to vote.
4.84/5 (44 votes)
2 May 20061 min read 143.9K   5.2K   88  
A VB.NET class for drawing text on a path.
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Drawing.Drawing2D
Public Enum PathAlign
    Left = 0
    Center = 1
    Right = 2
End Enum
Public Enum TextPosition
    OverPath = 0
    CenterPath = 1
    UnderPath = 2
End Enum
Public Class TextOnPath
    Private _pathdataTOP As PathData
    Private _textTOP As String
    Private _fontTOP As Font
    Private _pathcolorTOP As Color = Color.Red
    Private _colorTOP As Color = Color.Black
    Private _fillcolorTOP As Color = Color.Black
    Private _pathalignTOP As PathAlign = PathAlign.Center
    Private _letterspacepercentageTOP As Integer = 100
    Private _addsvg As Boolean = False
    Private _SVG As System.Text.StringBuilder
    Private _currentPathID As Integer
    Private _textPathPosition As TextPosition = TextPosition.CenterPath
    Public LastError As Exception = Nothing
    Public ShowPath As Boolean = True
    Public Property TextPathPosition() As TextPosition
        Get
            TextPathPosition = _textPathPosition
        End Get
        Set(ByVal value As TextPosition)
            _textPathPosition = value
        End Set
    End Property
    Public Property PathDataTOP() As PathData
        Get
            PathDataTOP = _pathdataTOP
        End Get
        Set(ByVal value As PathData)
            _pathdataTOP = value
        End Set
    End Property

    Public Property TextTOP() As String
        Get
            TextTOP = _textTOP
        End Get
        Set(ByVal value As String)
            _textTOP = value
        End Set
    End Property

    Public Property FontTOP() As Font
        Get
            FontTOP = _fontTOP
        End Get
        Set(ByVal value As Font)
            _fontTOP = value
        End Set
    End Property

    Public Property PathColorTOP() As Color
        Get
            PathColorTOP = _pathcolorTOP
        End Get
        Set(ByVal value As Color)
            _pathcolorTOP = value
        End Set
    End Property
    Public Property ColorTOP() As Color
        Get
            ColorTOP = _colorTOP
        End Get
        Set(ByVal value As Color)
            _colorTOP = value
        End Set
    End Property

    Public Property FillColorTOP() As Color
        Get
            FillColorTOP = _fillcolorTOP
        End Get
        Set(ByVal value As Color)
            _fillcolorTOP = value
        End Set
    End Property

    Public Property PathAlignTOP() As PathAlign
        Get
            PathAlignTOP = _pathalignTOP
        End Get
        Set(ByVal value As PathAlign)
            _pathalignTOP = value
        End Set
    End Property

    Public Property LetterSpacePercentage() As Integer
        Get
            LetterSpacePercentage = _letterspacepercentageTOP
        End Get
        Set(ByVal value As Integer)
            _letterspacepercentageTOP = value
        End Set
    End Property
    Public Function TextOnPathBitmap(ByVal _pathdata As PathData, ByVal _text As String, ByVal _font As Font, ByVal _color As Color, ByVal _fillcolor As Color, ByVal _letterspacepercentage As Integer) As Bitmap

        _pathdataTOP = _pathdata
        _textTOP = _text
        _fontTOP = _font
        _colorTOP = _color
        _fillcolorTOP = _fillcolor
        _letterspacepercentageTOP = _letterspacepercentage
        Return TextOnPathBitmap()

    End Function
    Public Function TextOnPathBitmap() As Bitmap
        Dim i As Integer
        Dim _TmpPoints() As PointF
        Dim _TmpPoint As PointF
        Dim _Points(25000) As PointF '= oGP.PathPoints()
        Dim _count As Integer
        Dim _gp As GraphicsPath = New GraphicsPath(_pathdataTOP.Points, _pathdataTOP.Types)
        _gp.FillMode = FillMode.Winding
        _gp.Flatten(Nothing, 1)
        Try
            _TmpPoint = _gp.PathPoints(0)
            For i = 0 To _gp.PathPoints.Length - 2
                If _gp.PathTypes(i + 1) = PathPointType.Start Or (_gp.PathTypes(i) And PathPointType.CloseSubpath) = PathPointType.CloseSubpath Then
                    _TmpPoints = GetLinePoints(_gp.PathPoints(i), _TmpPoint, 1)
                    Array.ConstrainedCopy(_TmpPoints, 0, _Points, _count, _TmpPoints.Length)
                    _count += 1
                    _TmpPoint = _gp.PathPoints(i + 1)
                Else
                    _TmpPoints = GetLinePoints(_gp.PathPoints(i), _gp.PathPoints(i + 1), 1)
                    Array.ConstrainedCopy(_TmpPoints, 0, _Points, _count, _TmpPoints.Length)
                    _count += _TmpPoints.Length - 1
                End If

            Next
            ReDim _TmpPoints(_count)
            Array.Copy(_Points, _TmpPoints, _count)
            _Points = CleanPoints(_TmpPoints)

            _count = _Points.Length - 1
            Return DrawText(_Points, _count)
        Catch ex As Exception
            LastError = ex
            Return Nothing
        End Try

    End Function
    Private Function CleanPoints(ByVal _points() As PointF) As PointF()

        Dim i As Integer
        Dim _tmppoints(_points.Length) As PointF
        Dim _lastpoint As PointF
        Dim _count As Integer

        For i = 0 To _points.Length - 1
            If i = 0 Or _points(i).X <> _lastpoint.X Or _points(i).Y <> _lastpoint.Y Then
                _tmppoints(_count) = _points(i)
                _count += 1
            End If
            _lastpoint = _points(i)
        Next

        ReDim _points(_count)
        Array.Copy(_tmppoints, _points, _count)
        Return _points

    End Function

    Private Function DrawText(ByVal _Points() As PointF, ByVal _MaxPoints As Integer) As Bitmap

        Dim _gp As GraphicsPath = New GraphicsPath(_pathdataTOP.Points, _pathdataTOP.Types)
        _gp.FillMode = FillMode.Winding
        _gp.Flatten()
        Dim _bitmap As Bitmap = New Bitmap(CInt(_gp.GetBounds.Right + _fontTOP.Size * 2), CInt(_gp.GetBounds.Bottom + _fontTOP.Size * 2))
        _gp.Dispose()
        Dim _G As Graphics = Graphics.FromImage(_bitmap)
        Dim _count As Integer
        Dim _Point1 As PointF
        Dim _Point2 As PointF
        Dim _Point As PointF
        Dim _CharStep As Integer
        Dim lStrWidth As Integer
        Dim _Angle As Decimal
        Dim _MaxWidthText As Decimal
        Dim i As Integer
        Dim _widths As Single()
        '_widths = MeasureWidths(_G)
        Dim _pathpen As Pen = New Pen(_pathcolorTOP)
        If ShowPath = True Then
            For Each _Point1 In _Points
                _G.DrawEllipse(_pathpen, _Point1.X, _Point1.Y, 1, 1)
            Next
        End If
        _pathpen.Dispose()
        For i = 0 To _textTOP.Length - 1
            _MaxWidthText += StringRegion(_G, i) ' _widths(i)
        Next

        Select Case _pathalignTOP
            Case PathAlign.Left
                _Point1 = _Points(0)
                _count = 0
            Case PathAlign.Center
                _count = (_MaxPoints - _MaxWidthText) \ 2
                If _count > 0 Then
                    _Point1 = _Points(_count)
                Else
                    _Point1 = _Points(0)
                End If
            Case PathAlign.Right
                _count = (_MaxPoints - _MaxWidthText - StringRegion(_G, _textTOP.Length - 1) * LetterSpacePercentage / 100)
                If _count > 0 Then
                    _Point1 = _Points(_count)
                Else
                    _Point1 = _Points(0)
                End If
        End Select

        Do Until _CharStep > _textTOP.Length - 1
            lStrWidth = StringRegion(_G, _CharStep) * LetterSpacePercentage / 100
            If (_count + lStrWidth \ 2) >= 0 And (_count + lStrWidth) < _MaxPoints Then
                _count += lStrWidth
                _Point2 = _Points(_count)
                _Point = _Points(_count - lStrWidth \ 2)
                _Angle = GetAngle(_Point1, _Point2)
                DrawRotatedText(_G, _textTOP.Chars(_CharStep), _Angle, _Point)   '
                _Point1 = _Points(_count)
                '_G.DrawEllipse(Pens.Red, oPoint1.X, oPoint1.Y, 2, 2)
            Else
                _count += lStrWidth
            End If
            _CharStep += 1
        Loop
        _G.Dispose()
        Return _bitmap
    End Function

    Private Function StringRegion(ByVal _g As Graphics, ByVal _textpos As Integer) As Single

        Dim measureString As String = _textTOP.Substring(_textpos, 1)
        Dim numChars As Integer = measureString.Length
        Dim characterRanges(numChars) As CharacterRange
        Dim stringFormat As StringFormat = New StringFormat
        stringFormat.Trimming = StringTrimming.None
        stringFormat.FormatFlags = StringFormatFlags.NoClip Or StringFormatFlags.NoWrap Or StringFormatFlags.LineLimit
        Dim size As SizeF = _g.MeasureString(_textTOP, _fontTOP, _fontTOP.Style)
        Dim layoutRect As RectangleF = New RectangleF(0.0F, 0.0F, size.Width, size.Height)
        Dim stringRegions(numChars) As Region
        characterRanges(0) = New CharacterRange(0, 1)
        stringFormat.FormatFlags = StringFormatFlags.NoClip
        stringFormat.SetMeasurableCharacterRanges(characterRanges)
        stringFormat.Alignment = StringAlignment.Near
        stringRegions = _g.MeasureCharacterRanges(_textTOP.Substring(_textpos), _fontTOP, layoutRect, stringFormat)
        Return stringRegions(0).GetBounds(_g).Width
    End Function

    Private Function MeasureWidths(ByVal graphics As Graphics) As Single()
        Dim widths(_textTOP.Length) As Single
        Dim format As StringFormat = StringFormat.GenericTypographic
        format.Trimming = StringTrimming.None
        format.FormatFlags = StringFormatFlags.NoClip Or StringFormatFlags.NoWrap Or StringFormatFlags.LineLimit
        graphics.TextRenderingHint = Text.TextRenderingHint.AntiAlias

        Dim layout As RectangleF = New RectangleF(0, 0, 10000, 10000)
        Dim remainder As Integer = _textTOP.Length
        Dim start As Integer = 0
        While remainder > 0
            Dim length As Integer = remainder
            If length > 32 Then
                length = 32
            End If
            Dim ranges(length) As CharacterRange
            Dim i As Integer = 0
            While i < ranges.Length
                ranges(i) = New CharacterRange(start + i, 1)
                i += 1
            End While
            format.SetMeasurableCharacterRanges(ranges)
            Dim regions As Region() = graphics.MeasureCharacterRanges(_textTOP, _fontTOP, layout, format)
            i = 0
            While i < regions.Length
                Dim region As Region = regions(i)
                Dim cb As RectangleF = region.GetBounds(graphics)
                widths(start + i) = cb.Width
                i += 1
            End While
            start += length
            remainder -= 31
        End While
        Return widths
    End Function

    Private Function GetAngle(ByVal _point1 As PointF, ByVal _point2 As PointF) As Decimal
        Dim c As Decimal

        c = Math.Sqrt((_point2.X - _point1.X) ^ 2 + (_point2.Y - _point1.Y) ^ 2) 'Oh yeah good old math a�+b�=c�
        If c = 0 Then
            Return 0
        End If
        If _point1.X > _point2.X Then 'We must change the side where the triangle is
            Return Math.Asin((_point1.Y - _point2.Y) / c) * 180 / Math.PI - 180
            'Return _tangents(CInt((_point1.Y - _point2.Y) / c * 1800) + 1800) * 180 / Math.PI - 180
        Else
            Return Math.Asin((_point2.Y - _point1.Y) / c) * 180 / Math.PI
            'Return _tangents(CInt((_point2.Y - _point1.Y) / c * 1800) + 1800) * 180 / Math.PI
        End If


    End Function
    Private Sub DrawRotatedText(ByVal _gr As Graphics, ByVal _text As String, ByVal _angle As Single, ByVal _PointCenter As PointF)

        Dim string_format As New StringFormat
        string_format.Alignment = StringAlignment.Center

        _gr.SmoothingMode = SmoothingMode.HighQuality
        _gr.CompositingQuality = CompositingQuality.HighQuality
        _gr.TextRenderingHint = Text.TextRenderingHint.AntiAlias
        Dim graphics_path As New GraphicsPath(Drawing.Drawing2D.FillMode.Winding)
        Dim x As Integer = CInt(_PointCenter.X)
        Dim y As Integer = CInt(_PointCenter.Y)

        Select Case TextPathPosition
            Case TextPosition.OverPath
                graphics_path.AddString(_text, _fontTOP.FontFamily, _fontTOP.Style, _fontTOP.Size, New Point(x, y - _fontTOP.Size), string_format)
            Case TextPosition.CenterPath
                graphics_path.AddString(_text, _fontTOP.FontFamily, _fontTOP.Style, _fontTOP.Size, New Point(x, y - _fontTOP.Size \ 2), string_format)
            Case TextPosition.UnderPath
                graphics_path.AddString(_text, _fontTOP.FontFamily, _fontTOP.Style, _fontTOP.Size, New Point(x, y), string_format)
        End Select


        Dim rotation_matrix As New Matrix
        rotation_matrix.RotateAt(_angle, New PointF(x, y))
        graphics_path.Transform(rotation_matrix)

        _gr.DrawPath(New Pen(_colorTOP), graphics_path)
        _gr.FillPath(New SolidBrush(_fillcolorTOP), graphics_path)

        If _addsvg = True Then
            _SVG.Append(_createSVG(graphics_path))
        End If
        graphics_path.Dispose()
    End Sub

    Public Function GetSVG(ByVal _width As Integer, ByVal _height As Integer) As String

        Dim oXML As New System.Text.StringBuilder
        _SVG = New System.Text.StringBuilder
        _currentPathID = 1
        _addsvg = True
        TextOnPathBitmap()
        _addsvg = False

        _width = _width * 2
        _height = _height * 2
        oXML.Append("<?xml version=""1.0"" standalone=""no""?>")

        oXML.Append("<!DOCTYPE svg PUBLIC ""-//W3C//DTD SVG 20001102//EN"" ""http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd"">" & vbCrLf)
        oXML.Append("<svg width=""" & _width & """ height=""" & _height & """ viewBox=""0 0 " & _width & " " & _height & """>" & vbCrLf)
        oXML.Append("<desc>Hello World</desc>" & vbCrLf)
        oXML.Append(_SVG.ToString)
        oXML.Append("</svg>" & vbCrLf)

        Return oXML.ToString

    End Function
    
    Private Function _createSVG(ByVal _path As GraphicsPath) As String
        Dim _pathString As New System.Text.StringBuilder
        Dim i As Integer
        Dim pt As PointF
        If _path.PathData.Points.Length = 0 Then
            Return ""
        End If
        For i = 0 To _path.PathData.Points.Length - 1
            If _path.PathData.Types(i) = PathPointType.Start Then
                _pathString.Append("M " + Str(_path.PathData.Points(i).X) + " " + Str(_path.PathData.Points(i).Y) + "" & vbCrLf)
                pt = _path.PathData.Points(i)
            End If

            If (_path.PathData.Types(i) And PathPointType.Bezier) = PathPointType.Bezier Then
                _pathString.Append(" C " + Str(_path.PathData.Points(i).X) + " " + Str(_path.PathData.Points(i).Y) _
                 + " " + Str(_path.PathData.Points(i + 1).X) + " " + Str(_path.PathData.Points(i + 1).Y) _
                 + " " + Str(_path.PathData.Points(i + 2).X) + " " + Str(_path.PathData.Points(i + 2).Y) + "" & vbCrLf)
                If (_path.PathData.Types(i + 2) And PathPointType.CloseSubpath) = PathPointType.CloseSubpath Then
                    _pathString.Append(" C " + Str(_path.PathData.Points(i + 2).X) + " " + Str(_path.PathData.Points(i + 2).Y) _
                    + " " + Str(_path.PathData.Points(i + 2).X) + " " + Str(_path.PathData.Points(i + 2).Y) _
                    + " " + Str(pt.X) + " " + Str(pt.Y) + "" & vbCrLf)
                End If
                If (_path.PathData.Types(i) And PathPointType.CloseSubpath) = PathPointType.CloseSubpath Then
                    _pathString.Append(" L " + Str(pt.X) + " " + Str(pt.Y) + "" & vbCrLf)
                End If
                i += 2
            End If

            If (_path.PathData.Types(i) And PathPointType.Line) = PathPointType.Line Then
                _pathString.Append(" L " + Str(_path.PathData.Points(i).X) + " " + Str(_path.PathData.Points(i).Y) + "" & vbCrLf)
                If (_path.PathData.Types(i) And PathPointType.CloseSubpath) = PathPointType.CloseSubpath Then
                    _pathString.Append(" L " + Str(pt.X) + " " + Str(pt.Y) + "" & vbCrLf)
                End If
            End If

        Next
        _currentPathID += 1
        Return "<path id=""" + _currentPathID.ToString + """ d=""" + _pathString.ToString + """/>" & vbCrLf
    End Function
    Public Function GetLinePoints(ByVal _p1 As PointF, ByVal _p2 As PointF, ByVal _stepWitdth As Integer) As PointF()

        Dim lCount As Integer = 0
        Dim _tmpPoints(10000) As PointF
        Dim _width As Long
        Dim _height As Long
        Dim d As Long
        Dim ix As Long
        Dim iy As Long
        Dim dd As Integer
        Dim id As Integer
        Dim lStep As Integer = _stepWitdth

        _p1.X = CInt(_p1.X)
        _p1.Y = CInt(_p1.Y)
        _p2.X = CInt(_p2.X)
        _p2.Y = CInt(_p2.Y)
        _width = _p2.X - _p1.X
        _height = _p2.Y - _p1.Y
        d = 0

        If _width < 0 Then
            _width = -_width
            ix = -1
        Else
            ix = 1
        End If

        If _height < 0 Then
            _height = -_height
            iy = -1
        Else
            iy = 1
        End If

        If _width > _height Then
            dd = _width + _width
            id = _height + _height

            Do
                If lStep = _stepWitdth Then
                    _tmpPoints(lCount).X = _p1.X
                    _tmpPoints(lCount).Y = _p1.Y
                    lCount += 1
                Else
                    lStep = lStep + _stepWitdth
                End If
                If CInt(_p1.X) = CInt(_p2.X) Then Exit Do
                _p1.X = _p1.X + ix
                d = d + id

                If d > _width Then
                    _p1.Y = _p1.Y + iy
                    d = d - dd
                End If
            Loop

        Else
            dd = _height + _height
            id = _width + _width

            Do
                If lStep = _stepWitdth Then
                    _tmpPoints(lCount).X = _p1.X
                    _tmpPoints(lCount).Y = _p1.Y
                    lCount += 1
                Else
                    lStep = lStep + _stepWitdth
                End If
                If CInt(_p1.Y) = CInt(_p2.Y) Then Exit Do
                _p1.Y = _p1.Y + iy
                d = d + id

                If d > _height Then
                    _p1.X = _p1.X + ix
                    d = d - dd
                End If
            Loop
        End If

        Dim _tmpPoints2 As PointF()
        ReDim _tmpPoints2(lCount)
        Array.Copy(_tmpPoints, _tmpPoints2, lCount)
        Return _tmpPoints2

    End Function

End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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


Written By
Architect
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions