Left, Right, and Center. Those are the only options right? That might be how SSRS views the world, but nope...
![](https://phidiax.com/blog/image.axd?picture=%2f2017%2f09%2fWordJustifyToolbar.PNG)
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 in this entry, and the "poor man's" justification using added spaces in the next entry. The second 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.
Text Rendering As Image
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 much space between each word in that line is needed to present that line as justified. As each line is calculated, it is drawn to a Bitmap by a System.Drawing.Graphics object. There's a good set of code here I adapted to separate and draw the text for us.
Once the text is all rendered, the bitmap is converted to a stream of bytes and fed into an Image on the desired SSRS report. Note that to get a full 300dpi image or better for printing, you need to make sure you have plenty of free memory!
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):
![](https://phidiax.com/blog/image.axd?picture=%2f2017%2f09%2fAddReferencesSSRS.PNG)
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 Image:
Public Function picText_Paint(text As String, sizeX As Integer, sizeY As Integer, resolution As Integer, LineSpacing As Single, ExtraParagraphSpacing As Single, font As System.Drawing.Font, TextMargin As System.Windows.Forms.Padding, colorBrush As System.Drawing.Brush) As Byte()
'Create a temporary bitmap in memory to use as the drawing surface. Use provided size (in Px) taking difference between desired
'resolution and default screen based resolution. This should provide a better quality image for printing without using all of the
'machine's memory to render the image.
'If an error occurs due to the image sizing and memory, a "can't load this image" x will appear in this image's place on report
sizeX *= (resolution / 96)
sizeY *= (resolution / 96)
Dim bmp As New System.Drawing.Bitmap(sizeX, sizeY, Drawing.Imaging.PixelFormat.Format32bppArgb)
bmp.SetResolution(resolution, resolution)
'Create the graphics object for drawing from that temp bitmap and blank out the background.
Dim g As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(bmp)
'Set high quality rendering all around. If not smooth enough, try killing transparent background by clearing graphics with white.
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit
g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.HighQuality
g.InterpolationMode = Drawing.Drawing2D.InterpolationMode.HighQualityBicubic
g.CompositingQuality = Drawing.Drawing2D.CompositingQuality.HighQuality
' Draw within a rectangle excluding the margins.
Dim rect As New System.Drawing.RectangleF(TextMargin.Left, TextMargin.Top, sizeX - TextMargin.Left - TextMargin.Right, sizeY - TextMargin.Top - TextMargin.Bottom)
'Draw the paragraphs of the provided text. This will split and loop using carriage return/line feed.
rect = DrawParagraphs(g, rect, font, colorBrush, text, LineSpacing, 0, ExtraParagraphSpacing)
'Use a memory stream to write the bitmap to an array of bytes
Dim stream As System.IO.MemoryStream = New IO.MemoryStream
Dim bitmapBytes As Byte()
bmp.Save(stream, Drawing.Imaging.ImageFormat.Png)
bitmapBytes = stream.ToArray
stream.Dispose()
bmp.Dispose()
g.Dispose()
Return bitmapBytes
End Function
Draw Paragraphs method (called above) will loop until all text is written or the image is out of room based on size:
Private Function DrawParagraphs(gr As System.Drawing.Graphics, rect As System.Drawing.RectangleF, font As System.Drawing.Font, brush As System.Drawing.Brush, text As String, line_spacing As Single, indent As Single, paragraph_spacing As Single) As System.Drawing.RectangleF
' Split the text into paragraphs.
Dim paragraphs As String() = text.Split(ControlChars.Lf)
' Draw each paragraph.
For Each paragraph As String In paragraphs
' Draw the paragraph keeping track of remaining space.
rect = DrawParagraph(gr, rect, font, brush, paragraph,
line_spacing, indent, paragraph_spacing)
' See if there's any room left.
If rect.Height < gr.MeasureString("Hi", font).Height Then
Exit For
End If
Next
Return rect
End Function
Draw Paragraph (called above) will split each paragraph by lines based on what will fit on each:
Private Function DrawParagraph(gr As System.Drawing.Graphics, rect As System.Drawing.RectangleF, font As System.Drawing.Font, brush As System.Drawing.Brush, text As String, line_spacing As Single, indent As Single, extra_paragraph_spacing As Single) As System.Drawing.RectangleF
' Get the coordinates for the first line.
Dim y As Single = rect.Top
' Break the text into words.
Dim words As String() = text.Split(" "c)
Dim start_word As Integer = 0
Dim fh As Single
' 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)
fh = line_size.Height
If line_size.Width + indent > rect.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.
DrawLine(gr, line, font, brush, rect.Left + indent, y,
rect.Width - indent, False)
Else
' This is not the last line. Justify it.
DrawLine(gr, line, font, brush, rect.Left + indent, y,
rect.Width - indent, True)
End If
' Move down to draw the next line.
y += fh * line_spacing
' Make sure there's room for another line.
If fh > rect.Height Then
Exit While
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.
y += fh * extra_paragraph_spacing
' Return a RectangleF representing any unused
' space in the original RectangleF.
Dim height As Single = rect.Bottom - y
If height < 0 Then
height = 0
End If
Return New System.Drawing.RectangleF(rect.X, y, rect.Width, height)
End Function
DrawLine (called above) will draw the text in the image:
Private Sub DrawLine(gr As System.Drawing.Graphics, line As String, font As System.Drawing.Font, brush As System.Drawing.Brush, x As Single, y As Single,
width As Single, justification As Boolean)
' Make a rectangle to hold the text.
Dim rect As New System.Drawing.RectangleF(x, y, width, gr.MeasureString(line, font).Height)
' See if we should use full justification.
If justification Then
' Justify the text.
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 = rect.Width - total_width
Dim num_spaces As Integer = words.Length - 1
If words.Length > 1 Then
extra_space /= num_spaces
End If
' Draw the words.
Dim x1 As Single = rect.Left
Dim y1 As Single = rect.Top
For i As Integer = 0 To words.Length - 1
' Draw the word.
gr.DrawString(words(i), font, brush, x1, y1)
' Move right to draw the next word.
x1 += word_width(i) + extra_space
Next
Else
' Make a StringFormat to align the text.
Using sf As New System.Drawing.StringFormat()
' Use the appropriate alignment.
sf.Alignment = System.Drawing.StringAlignment.Near
gr.DrawString(line, font, brush, rect, sf)
End Using
End If
End Sub
Setup SSRS Report with Image
Now that we have the code in place and ready to go, we just need to create an Image on the SSRS report, and set it up to use the main method to create its content. This is where the trial and error comes in because we need to give a size in pixels since this is drawing using the Graphics object (you'll need to guess and tweak the width and height you provide the method until it matches what you are expecting to see. You can use your provided resolution * inches desired as a starting point.
Right-click the area in the report and select Insert -> Image. Setup the main screen as follows. It isn't the most intuitive, but the source of the image really does need to be "Database." Since the code is saving the bitmap as a memory stream in PNG format, we also select that:
![](https://phidiax.com/blog/image.axd?picture=%2f2017%2f09%2fImageSetupMainSSRS.PNG)
Now, select the Expression editor for "Use this field" to enter a reference to the custom code (filling in desired values in place of the italics labels below):
=Code.picText_Paint(text, _
width, _
height, _
resolution, _
line_spacing_multiplier, _
paragraph_spacing_multiplier, _
new System.Drawing.Font(font_name, font_pt_size), _
new System.Windows.Forms.Padding(image_edge_padding_pt), _
System.Drawing.Brushes.Black)
For best results, also select "Fit Proportional" on the Sizing tab.
Resulting Report
Now, for the part you waded the whole way through this blog for! The results! Here is a screenshot at 200% zoom of a text block and the justified image:
![](https://phidiax.com/blog/image.axd?picture=%2f2017%2f09%2fResult.PNG)
Hopefully 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. In the next entry, I will provide the additional solution on how to justify by inserting extra spacing in existing text for a "justified feel."