Generating PDF Reports within Rails using Prawn PDF and Prawn Table Gems.

Muwonge Nicholus
7 min readSep 14, 2023

--

Photo by Markus Spiske on Unsplash

When I’m working on different projects, I often have to create PDFs for important documents like transaction summaries and invoices. But don’t worry, it’s not as complicated as it sounds! In this blog post, I’ll show you a simple way to make PDFs. If you’re looking for PDF gems for Ruby, just search for “PDF” on RubyGems.org or LibHunt and you’ll find plenty of options.

Let's dive right in then. We will start by creating an empty new project

rails new prawn_pdf

After the project files have been generated, we will need to add the gems required to the Gemfile and run bundle install .

gem "prawn", "~> 2.4" # for generating pdfs
gem "prawn-table", "~> 0.2.2" # for adding tables to prawn pdfs, -
# because most reports have tables in them.
gem "prawn-icon", "~> 3.1" # You may need this if you want to use icons at all
gem "letter_opener", group: :development # add this under the development section.

You can find the links to the Gem GitHub pages at the end of this blog. Prawn Gem has a PDF doc and does Prawn Table Gem.

There are multiple ways to approach building a PDF, but I usually approach it like I am designing an email. I use tables a lot to align stuff really well across the page.

For this blog post, I found this random PDF online, and I will try to redesign it.

Report We’re trying to generate

Since I will be using the PDF throughout my entire Rails application, I will place it in a service. Let’s create one under `app/services/pdf_generator_service.rb`

The Initialization:

In this section, I set up everything that would need to be used throughout the pdf sections like the: fonts , page orientation and other components

class PdfGeneratorService
MONTSERRAT_FONT_PATH = 'app/assets/stylesheets/Montserrat-Medium.ttf'
MONTSERRAT_BOLD_FONT_PATH = 'app/assets/stylesheets/Montserrat-Black.ttf'
LOGO_IMG_PATH = 'app/assets/images/logo.png'
def initialize
@pdf = Prawn::Document.new
@document_width = @pdf.bounds.width # width fo the document
@pdf.font_families.update(
'montserrat' => {
normal: MONTSERRAT_FONT_PATH, # Path to the normal (regular) font style
bold: MONTSERRAT_BOLD_FONT_PATH # Path to the bold font style (if applicable)
}
)
end
end

In this case, I wanted to use a custom font called montserrat for which I downloaded .ttf a file from Google Fonts to use in this pdf. Prawn supports custom fonts as well.

The Header Section:

class PdfGeneratorService
...
def header
status_results = [%w[15 3 5 4 3], %w[TESTS FAILED PASSED BLOCKED UNKNOWN]]
text_colors = { 1 => 'C8102F', 2 => '83BC41', 3 => 'FDB357', 4 => 'BBBBBB' }

header_table = @pdf.make_table(status_results) do |table|
table.row(0).font_style = :bold
table.row(0).font = 'montserrat'

header_status_rows = [0, 1]
header_status_rows.each do |row_index|
text_colors.each do |column_index, color|
table.row(row_index).column(column_index).style(text_color: color)
end
end

table.row(0..1).border_width = 0
table.row(0..1).column(0).border_width = 1
table.row(0..1).column(0).border_color = 'c0c5ce'
table.row(0..1).column(0).borders = [:right]
table.row(0).size = 17
table.row(1).size = 11
end

header_column_widths = [@document_width * 2 / 3, @document_width * 1 / 3]
header_title_data = [['Execution Summary']]
header_title_options = {
column_widths: [@document_width],
row_colors: ['EDEFF5'],
cell_style: {
border_width: 0,
padding: [15, 12, 1, 20],
size: 20,
font: 'montserrat',
font_style: :normal
}
}

header_logo_data = [[header_table, { image: LOGO_IMG_PATH, position: :center, scale: 0.5, colspan: 6 }]]
header_logo_options = {
column_widths: header_column_widths,
row_colors: ['EDEFF5'],
cell_style: {
border_width: 2,
padding: [15, 15],
borders: [:bottom],
border_color: 'c9ced5'
}
}

@pdf.table(header_title_data, header_title_options)
@pdf.table(header_logo_data, header_logo_options)
end
...
end

So this gives us this and I know it is not the exact replica but due to limited time, this is how close I got;

The Header Section

Key takeaways;

  • Prawn allows tables can be nested into other tables

The Middle Section:

class PdfGeneratorService
...
def mid_section
mid_section_data = [['General', '', ''], ['Start time:', 'Duration:', 'Executed by:'],
["Apr 7, 2020 \n 10:33:57", '15m 9s', 'z-automation-sayvr@perfectmobile.com']]

mid_section_options = {
width: @document_width,
row_colors: ['ffffff'],
cell_style: {
border_width: 0,
borders: [:bottom],
border_color: 'c9ced5',
padding: [10, 15]
}
}

@pdf.table(mid_section_data, mid_section_options) do |table|
table.row(0).border_width = 0.5
table.row(1).text_color = '888892'
table.row(0).padding = [10, 15]
table.row(2).size = 11
end
end
...
end
This is what we would achieve with the code above.

The Results Section:

class PdfGeneratorService
...
def result_section
report_data = [['Report', '', 'View detailed list in new Reporting'],
['Name', 'Platform', 'Status'],
['gehpbatosid4pp_oak6f', "Apple Chibn \n f7euq", 'BLOCKED'],
['gehpbatosid4pp_oak6f', "Apple Chibn \n f7euq", 'PASSED']]

report_data_options = {
width: @document_width,
row_colors: ['ffffff'],
cell_style: {
border_width: 1,
borders: [:bottom],
border_color: 'c9ced5'
}
}

@pdf.table(report_data, report_data_options) do |table|
test_status = table.rows(1..-1).column(-1)

test_status.filter do |cell|
case cell.content
when 'BLOCKED'
cell.text_color = 'FDB357'
when 'PASSED'
cell.text_color = '83BC41'
# add more cases
end
end

table.row(0..1).border_width = 0.5
table.row(0).column(2).text_color = '2787c4'
table.row(0).column(2).size = 11
table.row(1).background_color = 'EDEFF5'
table.row(1..-1).column(1..2).align = :center
table.row(1).text_color = '465579'
table.row(1).size = 10
table.row(1..-1).padding = [10, 15]
table.row(0).padding = [7, 15]
end
end
...
end

We achieve this part with the code above.

The bottom section

And finally combining all the sections in the PDF.

class PdfGeneratorService
...
def generate_pdf
header
mid_section
lower_section
@pdf.render
end
...
end

The overall service.

# frozen_string_literal: true

class PdfGeneratorService
MONTSERRAT_FONT_PATH = 'app/assets/stylesheets/Montserrat-Medium.ttf'
MONTSERRAT_BOLD_FONT_PATH = 'app/assets/stylesheets/Montserrat-Black.ttf'
LOGO_IMG_PATH = 'app/assets/images/logo.png'
def initialize
@pdf = Prawn::Document.new
@document_width = @pdf.bounds.width
@pdf.font_families.update(
'montserrat' => {
normal: MONTSERRAT_FONT_PATH, # Path to the normal (regular) font style
bold: MONTSERRAT_BOLD_FONT_PATH # Path to the bold font style (if applicable)
}
)
end

def header
status_results = [%w[15 3 5 4 3], %w[TESTS FAILED PASSED BLOCKED UNKNOWN]]
text_colors = { 1 => 'C8102F', 2 => '83BC41', 3 => 'FDB357', 4 => 'BBBBBB' }

header_table = @pdf.make_table(status_results) do |table|
table.row(0).font_style = :bold
table.row(0).font = 'montserrat'

header_status_rows = [0, 1]
header_status_rows.each do |row_index|
text_colors.each do |column_index, color|
table.row(row_index).column(column_index).style(text_color: color)
end
end

table.row(0..1).border_width = 0
table.row(0..1).column(0).border_width = 1
table.row(0..1).column(0).border_color = 'c0c5ce'
table.row(0..1).column(0).borders = [:right]
table.row(0).size = 17
table.row(1).size = 11
end

header_column_widths = [@document_width * 2 / 3, @document_width * 1 / 3]
header_title_data = [['Execution Summary']]
header_title_options = {
column_widths: [@document_width],
row_colors: ['EDEFF5'],
cell_style: {
border_width: 0,
padding: [15, 12, 1, 20],
size: 20,
font: 'montserrat',
font_style: :normal
}
}

header_logo_data = [[header_table, { image: LOGO_IMG_PATH, position: :center, scale: 0.5, colspan: 6 }]]
header_logo_options = {
column_widths: header_column_widths,
row_colors: ['EDEFF5'],
cell_style: {
border_width: 2,
padding: [15, 15],
borders: [:bottom],
border_color: 'c9ced5'
}
}

@pdf.table(header_title_data, header_title_options)
@pdf.table(header_logo_data, header_logo_options)
end

def mid_section
mid_section_data = [['General', '', ''], ['Start time:', 'Duration:', 'Executed by:'],
["Apr 7, 2020 \n 10:33:57", '15m 9s', 'z-automation-sayvr@perfectmobile.com']]

mid_section_options = {
width: @document_width,
row_colors: ['ffffff'],
cell_style: {
border_width: 0,
borders: [:bottom],
border_color: 'c9ced5',
padding: [10, 15]
}
}

@pdf.table(mid_section_data, mid_section_options) do |table|
table.row(0).border_width = 0.5
table.row(1).text_color = '888892'
table.row(0).padding = [10, 15]
table.row(2).size = 11
end
end


def lower_section
report_data = [['Report', '', 'View detailed list in new Reporting'],
['Name', 'Platform', 'Status'],
['gehpbatosid4pp_oak6f', "Apple Chibn \n f7euq", 'BLOCKED'],
['gehpbatosid4pp_oak6f', "Apple Chibn \n f7euq", 'PASSED']]

report_data_options = {
width: @document_width,
row_colors: ['ffffff'],
cell_style: {
border_width: 1,
borders: [:bottom],
border_color: 'c9ced5'
}
}

@pdf.table(report_data, report_data_options) do |table|
test_status = table.rows(1..-1).column(-1)
test_status.filter do |cell|
case cell.content
when 'BLOCKED'
cell.text_color = 'FDB357'
when 'PASSED'
cell.text_color = '83BC41'
end
end

table.row(0..1).border_width = 0.5
table.row(0).column(2).text_color = '2787c4'
table.row(0).column(2).size = 11
table.row(1).background_color = 'EDEFF5'
table.row(1..-1).column(1..2).align = :center
table.row(1).text_color = '465579'
table.row(1).size = 10
table.row(1..-1).padding = [10, 15]
table.row(0).padding = [7, 15]
end
end

def generate_pdf
header
mid_section
lower_section
@pdf.render
end
end

A lot of things could have been done better, in this class:

  • Create classes for each section and also put together similar styles and reuse them

Let’s create a controller to get this functionality working

Controllers:

class PdfGeneratorController < ApplicationController
def index;end

def generate_pdf
pdf_service = PdfGeneratorService.new
pdf_content = pdf_service.generate_pdf
pdf_filename = "test.pdf"

PdfMailer.send_report.deliver_now
File.open('test.pdf', 'wb') { |file| file.write(pdf_content) }

respond_to do |format|
format.pdf do
send_data pdf_content, filename: pdf_filename, type: 'application/pdf', disposition: 'attachment'
end
end
end
end

For sending the PDF attached to a mail. app/mailers/pdf_mailer.rb

Mailers:

class PdfMailer < ApplicationMailer
def send_report
@message = "Hey this is a result from your test suite."
pdf_content_path = generate_pdf_content

attachments['test_result_report.pdf'] = File.read(pdf_content_path)

mail(to: 'email@gmail.com', subject: 'Test Report Results.')
end

private

def generate_pdf_content
pdf_service = PdfGeneratorService.new
pdf = pdf_service.generate_pdf
pdf_file = Tempfile.new(['test_result_report', '.pdf'], Rails.root.join('tmp')) # store the generated pdf in tmp folder to be attached when sending mail.
pdf_file.binmode # converts the document content to binary mode. When you set a file to binary mode using binmode, it ensures that no newline character translations or character encoding conversions occur when reading or writing data
pdf_file.write(pdf)
pdf_file.rewind # move cursor to the top start of the page
pdf_file.close
pdf_file.path # generated pdf path
end
end

Binmode : When you set a file to binary mode using binmode, it ensures that no newline character translations or character encoding conversions occur when reading or writing data.

Views:

`app/views/pdf_generator/index.html.erb`

<div>
<h1>The PDF generator</h1>
<%= link_to 'Generate PDF', generate_pdf_path(format: :pdf), class: 'btn btn-primary' %>
</div>

`app/views/pdf_mailer/send_report.html.erb`

<div>
<h1>The PDF generator Tutorial</h1>
<p>
<%= @message %>
</p>
</div>

The final result would look like this;

This is what the final result would look like for the mailer and the pdf overall.

Extra tasks you could try out;

  • Add icons, using prawn-icon
  • Add footers at the bottom
  • Use dynamic data from an actual source like a Model.

Important Links:

You can find the working example via https://github.com/NicholusMuwonge/prawn_pdf. Feel free to comment with any questions and enhancements, I’m happy to learn from you all as well.

LinkedIn: linkedin.com/in/muwonge-nicholus-868468144/

Cheers 🥂.

--

--

Muwonge Nicholus
Muwonge Nicholus

Written by Muwonge Nicholus

I am a Javascript and Ruby engineer. I love designing user interfaces and currently working in payment systems.

No responses yet