Friday 17 March 2017

Applied Rails: Bulleted Text With Prawn

I use the prawn gem to generate pdf documents in my Rails application. It has a drawback that it does not have built-in support for displaying bulleted text. So I wrote a simple function that took a string parameter and printed it with an indent and a leading asterisk.
def bullet_item string
    indent 15, 0 do
        text "* " + string, :align => :justify
    end
end
So you get output like:

But this solution had an issue if the sentences are long. The second line starts at same indent as the first line, that is, it gets aligned to the star. Users are familiar with Office documents in which the first line and second line of bulleted text start at the same indentation.

See the following output:

To fix the problem, I split the string into two, as follows:
def bullet_item_2 string
    if string.length > 110
        sub_str = string[85..string.length-1]
        space_index = sub_str.index(' ')
        str1 = string[0..85+space_index]
        str2 = string[86+space_index..string.length-1]
        indent 10, 0 do
            text "* " + str1, :align => :justify
            indent(9) {text str2, :align => :justify}
        end
    else
        indent 10, 0 do
            text "* " + string, :align => :justify
        end
    end
end
In here, I split the string into two, if it is more than 110 characters long. I first get the index of the space character after the 85th character and use that position to split. I then print both the strings at the same indentation.

The output looks like:

But the problem did not go away. I was told that some of the sentences could be really long, perhaps would span three or four lines. Here is such text, taken from Wikipedia, that I will use to illustrate:
A Welding Procedure Specification (WPS) is the formal written document describing welding procedures, which provides direction to the welder or welding operators for making sound and quality production welds as per the code requirements . The purpose of the document is to guide welders to the accepted procedures so that repeatable and trusted welding techniques are used. A WPS is developed for each material alloy and for each welding type used. Specific codes and/or engineering societies are often the driving force behind the development of a company's WPS. A WPS is supported by a Procedure Qualification Record (PQR or WPQR). A PQR is a record of a test weld performed and tested (more rigorously) to ensure that the procedure will produce a good weld. Individual welders are certified with a qualification test documented in a Welder Qualification Test Record (WQTR) that shows they have the understanding and demonstrated ability to work within the specified WPS.
Back to the keyboard again I went, and came up with another solution, as given below:
def extractFirstPart(str)
    return str if str.length < 110
    substr = str[85..str.length-1]
    space_index = substr.index(' ')

    if space_index != nil
        return str[0..85+space_index]
    else
        return str
    end
end

def bullet_item_3 string
    counter = 1
    myStr = string.clone
    while myStr.length > 0 do
        str = extractFirstPart myStr
        if counter == 1
            indent 10, 0 do
                text "* " + str, :align => :justify
            end
        else
            indent(19) {text str, :align => :justify}
        end
        myStr.slice! str
        counter += 1
    end
end
This time, I extracted sub-strings in a loop and printed them with indentation. The problem of any length string was solved, but not the text-alignment issue. This latest solution made the problem even worse, as lines were cut at the right end at different positions.

This is how it looked like:
Finally, I realized that an aligned and justified bulleted text is nothing but a table without borders. We can have the asterisk in the first column and the text in the second column. Since the prawn table functions render text properly in the cells, this might work.
def bullet_text_with_borderless_table data
    bullet_data = []
    data.each {|d| bullet_data << ["*", d]}
    t = make_table bullet_data, {:cell_style => { :borders => [], :align => :justify, :padding => [0,10,0,0]}, header: true}
    indent(20) {t.draw}
end
And it worked..! Sometimes in programming life, dirty hacks are required to get clean output. Here is how my text gets displayed now:
Here is --> the file that has all three solutions. The output pdf is --> here.

1 comment:

  1. DZone republished this article. Please find it at:
    https://dzone.com/articles/applied-rails-bulleted-text-with-prawn

    ReplyDelete