Prawn is an amazingly configurable and flexible tool for PDF generation. It allows you to create boring financial style PDFs that can almost mimic a (static) Excel spreadsheet, and can also create a convincing party invitation letter.
One of Prawn's many tools to allow this kind of flexibility is the Repeater. It allows you to define a section of your PDF, and reuse it over and over, essentially stamping it on every page (or just on odd pages, or on the first 10 pages, etc, you can also configure that).
Let's create a simple PDF document as an example:
app/pdfs/base_pdf.rb:
class BasePdf < Prawn::Document
def initialize
super()
generate_pdf
end
def generate_pdf
repeat :all do
text 'Test'
end
3.times { start_new_page }
end
end
app/controllers/application_controller.rb:
def report
send_data BasePdf.new.render, type: 'application/pdf', disposition: 'inline'
end
Now we need to go to localhost:3000/report.pdf (after configuring the route) and the PDF will be displayed, which will contain 3 pages with the same 'Test' text printed on each of them.
Let's now understand how this happens: When that code calls repeat :all
, and pass a block to it, it's essentially telling Prawn to:
- Create a new
Stamp
(think of it as an actual stamp). - Repeat this stamp on all pages of the PDF (3 in our example). It also doesn't matter if you called
repeat
after callingstart_new_page
before, it will render the repeater on every page. - Render whatever is inside the block into this new stamp (in our case a simple text), so when Prawn "stamps" it it will stamp exactly the same thing over and over.
However, this "stamping" doesn't happen immediately, nor does it happen when a new page is started. Prawn will only "stamp" the repeater after the whole document has been defined, when the method render
is called. Usually you want to call render
when you want to send the PDF to the user, so it will always be at the end.
What if you want to change what's inside this Stamp
? Let's image a new situation, more akin to real world, where I have a PDF with many pages, and I need to print different information on the header for different pages. If you already know the exact number of pages the PDF will have and which page have which information, you can do something like this:
app/pdfs/base_pdf.rb
def header
repeat :all, dynamic: true do
if (page_number < 5)
text 'One thing'
else
text 'Other thing'
end
end
end
This is all fine and dandy, until you get yourself into a situation like this:
app/pdfs/base_pdf.rb
# I want to print the name of the country of the first city in the current page
def header_country
repeat :all, dynamic: true do
text @current_country
end
end
# We'll assume cities is a hash, where each key is a country and contains an array of cities for each country
def print_cities_of_the_world(all_cities)
all_cities.each do |country, cities|
@current_country = country
cities.each do |city|
text city
end
end
end
That's why dynamic repeaters exist. They allow you to change the contents of the stamp for each page.
What is the better solution? Struggling with this exact problem and that was the worst cliffhanger in my life