Different background images and page layouts in a single PDF with Prawn

The Prawn PDF library contains an option for adding arbitrary images as background onto each page of a PDF. Unfortunately this option cannot be customized further leading to a couple of limitations that we worked around at my company. The letters and PDFs sent out always have different page layouts. The first page contains a much more detailed background image containing things like addresses, bank information and other information required by German law. The second and all following pages only contain a stripped-down version of that background image. As the first page also contains the recipient’s address  the area containing the actual text differs from one page to another.

Achieving this with Prawn is rather easy if you know that all of Prawn’s own rendering functions like text or table use the start_new_page method whenever the current page is full. As start_new_page accepts an options hash for modifying the page layout of the new page it obvious that this is the place we have to hook in to. Here’s the code that lets us do that:

module Prawn
  class Document
    alias_method :orig_start_new_page, :start_new_page unless method_defined? :orig_start_new_page

    def run_before_new_page(&block)
      @run_before_new_page_blocks ||= Array.new
      @run_before_new_page_blocks <<  Proc.new(&block)
    end

    def run_after_new_page(&block)
      @run_after_new_page_blocks ||= Array.new
      @run_after_new_page_blocks <<  Proc.new(&block)
    end

    def start_new_page(options = {})
      run_blocks @run_before_new_page_blocks, options
      self.orig_start_new_page options
      run_blocks @run_after_new_page_blocks
    end

    protected

    def run_blocks(blocks, *args)
      return if !blocks || blocks.empty?
      blocks.each { |block| block.arity == 0 ? self.instance_eval(&block) : block.call(self, *args) }
    end
  end
end

Here's a short explanation:

  1. In line 3 we rename Prawn's original start_new_page to orig_start_new_page because we want to use it later on in our own version of start_new_page.
  2. Two new methods run_before_new_page and run_after_new_page provide the hooks to the users of the Prawn::Document class. The first hook is run before the original start_new_page method is called while the second hook is run afterwards. The before hook can be used to modify the options hash passed to start_new_method (this is were the page's bounding box can be changed) and the after hook is used for displaying pictures.

But how is this used for actual content? That's easy:

Prawn::Document.generate "start_new_page_demo.pdf", :page_size => "A4" do
  run_before_new_page do |pdf, options|
    options[:top_margin] = cm2pt(4.0)
  end

  run_after_new_page do
    image "background_page2_200dpi.png",
      :at  => [-bounds.absolute_left, Prawn::Document::SIZES["A4"][1] - bounds.absolute_bottom],
      :fit => Prawn::Document::SIZES["A4"]
  end

  # This image only appears on the first page.
  image "background_page1_200dpi.png",
    :at  => [-bounds.absolute_left, Prawn::Document::SIZES["A4"][1] - bounds.absolute_bottom],
    :fit => Prawn::Document::SIZES["A4"]

  # Draw enough text to ensure that a second page is created.
  text "Hello, page 1!\n\n"
  text "lorem ipsum" * 400
  text "\nAnd now we're on another page."
end

You'll notice several things:

  1. The blocks passed to run_before_new_page and run_after_new_page can be used with or without local parameters. If you want to modify the options hash then you have to use local variables for run_before_new_page, but you usually don't need them for run_after_new_page. The latter also only takes a single argument, the Prawn::Document instance usually named "pdf".
  2. Both block types are not called for the very first page. The reason is that Prawn::Document's initialize method calls start_new_page before the hooks are registered themselves. However, this is fortunate for us because it makes switching background images easier.
  3. I'm letting Prawn scale the images to the page size. This is done because the images themselves have a very high resolution so that zooming into the PDF will not make them look bad even at low zoom levels.

If you're curious what the result looks like then download the resulting PDF. It was generated with a more elaborate version of the code pasted above which in turn can be downloaded here as well. Please note that the images have been smudged on purpose for this demonstration -- the originals look just fine.

A final note if you want to use this in a Rails project: simply put the extension to the Prawn::Document class into a file in the config/initializers directory, e.g. config/initializers/prawn_extensions.rb. Don't forget to restart the server afterwards or it won't pick up the new file.

One thought on “Different background images and page layouts in a single PDF with Prawn

Comments are closed.