Phidiax Tech Blog

Adventures in custom software and technology implementation.

SQL Server Reporting Services (SSRS): How to Justify Text (Part 2)

Left, Right, and Center. Those are the only options right? That might be how SSRS views the world, but nope...

Full justification basically just means that both the left and the right sides of the text end at exactly the same place on the page, regardless of the line length. Word does it. Publisher does it. Even HTML and CSS can do it. (You'll notice this is the only entry on the Phidiax blog that is full justified)

Unfortunately for those using SSRS to render extended text blocks from a database, SSRS just can't do it. At least not out of the box. Here's where some Googling (or Binging depending on your search engine preference), some language conversion, and some SSRS knowledge can come in handy to make all your text justification dreams come true.

There are two ways we can accomplish this (well, without having to buy custom SSRS components or switch to Crystal Reports). I'll cover them both... the best looking way (here in part 1), and the "poor man's" justification using added spaces in this entry. This method  is provided solely in the event that your user base wants the rendered result to still be "text." It is not as accurate, rather gives the "feeling" of justified text. It leaves you at the mercy of the font's implementation of the Unicode "hair space" character and how well that evenly divides into the amount of required "empty space" between words. This was developed because I had a client in just this situation: wanting justified text without converting to an image in the resulting rendered document. Admittedly, keeping it as text provides superior quality when printing or exporting to PDF, especially in limited memory environments.

Adding Hair Spacing To Text

So to justify text, we basically need to divide the text into paragraphs by carriage returns, then by words with spaces, and measure the size of how each word will render in the selected font to determine how many words will fit on each line, and as a result, how many Unicode "hair space" characters are needed between each word in that line to present that line as justified. As each line is calculated, it has the regular space characters replaced with hair space characters. Once the space "goes over" expected space, additional spacing is tested in a subset of the text only to get as close as possible to the correct measurement. There's a good set of code here I adapted to separate and alter the text for us.

Once the text is all calculated and updated, the text is fed into a TextBox/TextRun in the SSRS report. 


Setup Report Custom Code

To add the code needed to accomplish the task at hand, you will need to open the desired report, and go into the Report Properties and the References tab (note: screen shots are taken using Report Builder 2016). Add references to System.Drawing.dll and System.Windows.Forms.dll using the local GAC folder and finding the subfolders for each dll (default: C:\Windows\Microsoft.NET\assembly\GAC_MSIL):


Now that we have the necessary references, we need to paste in our custom code. For those not familiar with SSRS and custom code, it uses the Visual Basic language, can't have any "Import" statements at the top, and has absolutely no syntax highlighting or intellisense. I'd advise writing the code in Visual Studio or VS Code before attempting to paste into the report's code block. So switch to the Code tab on the Report Properties window and paste in each of the following methods.

Main method called by Text Run Expression:
Public Function TextOnlyJustify(text As String, font As System.Drawing.Font, bIndent As Boolean, width As Single) As String
	Dim bmp As New System.Drawing.Bitmap(1024, 1024)
	Dim gr = System.Drawing.Graphics.FromImage(bmp)
	Dim sRtn As New System.Text.StringBuilder()

	Dim paragraphs As String() = text.Split(ControlChars.NewLine)

	For Each paragraph As String In paragraphs
		Dim words As String() = paragraph.Split(" "C)
		Dim start_word As Integer = 0
		Dim indent As Single = If(bIndent, 40F, 0F)

		' Repeat until we run out of text or room.
		While True
			' See how many words will fit.
			' Start with just the next word.
			Dim line As String = words(start_word)

			' Add more words until the line won't fit.
			Dim end_word As Integer = start_word + 1
			While end_word < words.Length
				' See if the next word fits.
				Dim test_line As String = (line & Convert.ToString(" ")) + words(end_word)
				Dim line_size As System.Drawing.SizeF = gr.MeasureString(test_line, font)
				If line_size.Width + indent > width Then
					' The line is too wide. Don't use the last word.
					end_word -= 1
					Exit While
				Else
					' The word fits. Save the test line.
					line = test_line
				End If

				' Try the next word.
				end_word += 1
			End While

			' See if this is the last line in the paragraph.
			If (end_word = words.Length) Then
				' This is the last line. Don't justify it.
				sRtn.Append(line)
			Else
				' This is not the last line. Justify it.

				sRtn.Append(TextLine(gr, line, font, width - indent, True))
			End If

			' Start the next line at the next word.
			start_word = end_word + 1
			If start_word >= words.Length Then
				Exit While
			End If

			' Don't indent subsequent lines in this paragraph.
			indent = 0
		End While

		' Add a gap after the paragraph.
		sRtn.Append(vbLf & vbCr)
	Next

	Return sRtn.ToString()
End Function

TextLine method (called above) will write text line with extra spacing as a string:
    Public Function TextLine(gr As System.Drawing.Graphics, line As String, font As System.Drawing.Font, width As Single, justification As Boolean) As String
        Dim sLine As New System.Text.StringBuilder()
        ' See if we should use full justification.
        If justification Then
            ' Justify the text.
            ' Break the text into words.
            Dim words As String() = line.Split(" "c)

            ' Add a space to each word and get their lengths.
            Dim word_width As Single() = New Single(words.Length - 1) {}
            Dim total_width As Single = 0
            For i As Integer = 0 To words.Length - 1
                ' See how wide this word is.
                Dim size As System.Drawing.SizeF = gr.MeasureString(words(i), font)
                word_width(i) = size.Width
                total_width += word_width(i)
            Next

            ' Get the additional spacing between words.
            Dim extra_space As Single = width - total_width
            Dim num_spaces As Integer = words.Length - 1
            If words.Length > 1 Then
                extra_space /= (num_spaces-1)
            End If

            For i2 As Integer = 1 To 100
                Dim sTest As String = line.Replace(" ", New String(ChrW(&H200A), i2))
                If gr.MeasureString(sTest, font).Width > width Then

                    For i3 As Integer = words.Length To 1 Step -1
                        sTest = line.Replace(" ", New String(ChrW(&H200A), i2 - 1))
                        Dim sTemp = ReplaceSome(sTest, New String(ChrW(&H200A), i2 - 1), New String(ChrW(&H200A), i2), i3)
                        If gr.MeasureString(sTemp, font).Width < width Then
                            Console.WriteLine("{0}, size: {1}", line, gr.MeasureString(sTemp, font).Width)
                            Return sTemp + ControlChars.CrLf
                        End If
                    Next

                    Console.WriteLine("{0}, size: {1}", line, gr.MeasureString(line.Replace(" ", New String(ChrW(&H200A), i2 - 1)), font).Width)
                    Return line.Replace(" ", New String(ChrW(&H200A), i2 - 1)) + ControlChars.CrLf
                End If
            Next


        Else
            Return line
        End If
    End Function
ReplaceSome method called above to replace only a specific number of spaces with additional hair spaces to match expected line ends as closely as possible:
    Private Function ReplaceSome(s As String, repl As String, wth As String, num As Integer) As String

        ReplaceSome = String.Empty
        Dim s2 As String() = s.Split(repl, num, StringSplitOptions.RemoveEmptyEntries)

        For t As Integer = 0 To s2.Length - 2
            ReplaceSome += s2(t) + wth
        Next

        ReplaceSome += s2(s2.Length - 1)
    End Function


Setup SSRS Report with TextBox

Now that we have the code in place and ready to go, we just need to create TextBox or TextRun on the SSRS report, and set it up to use the main method to create its content.

Right-click the area in the report and select Insert -> TextBox. Right-click the text box and select Expression... and enter a reference to the custom code (filling in desired values in place of the italics labels below):Variables!GiantText.Value + vbcrlf+"Blah de blah de blahblahblah! I hope this formats well here!", new System.Drawing.Font("Calibri",10.5), false, 300)

=Code.TextOnlyJustify(text, _
new System.Drawing.Font(font_name, font_size), _ indent_true_false, _ width_px)

For best results, resize the textbox width to at least the expected width. Since carriage returns are inserted during formatting the text with spaces, resizing beyond the expected should not affect the line formats.


Resulting Report

Now, for the part you waded the whole way through this blog for! The results! 


Keep in mind that every font and text may result in a bit different appearance because the justification depends on the hair space character. This one here looks decent (The lines are really close to actual justification. This uses Calibri 10.5pt), but it just as easy to provide an example of one that looks awful using this method due to font sizing and characteristics.

Hopefully this (or this) will help those out there in a jam on how to justify text within an SSRS report without having to yank out your hair or fork out beaucoup bucks on custom components.

Loading

Privacy Policy  |  Contact  |  Careers

2009-2017 Phidiax, LLC - All Rights Reserved