Phidiax Tech Blog

Adventures in custom software and technology implementation.

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

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 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):


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:

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:



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."

Pingbacks and trackbacks (1)+

Loading

Privacy Policy  |  Contact  |  Careers

2009-2017 Phidiax, LLC - All Rights Reserved