0.0
No commit activity in last 3 years
No release in over 3 years
Dressing for Prawn
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.3
~> 2.6.1
~> 4.3.1
>= 0
~> 3.0
~> 0.8.7

Runtime

~> 1.3.0
~> 0.2.1
 Project Readme

Thousand Island

Dressing for Prawn

Gem Version Code Climate Test Coverage Build Status Inline docs

Prawn is awesome. It has some amazing functionality, and you can get anything that's in your head onto a PDF document with some Ruby code. For me though, as wonderful as that is, I normally only need a repeating header and footer, and then some text and maybe a table in between them. This is where Thousand Island comes in. A few simple commands should get you set up with a template that you can use application wide, and then all you need to worry about is getting the right content into the document.

Note: ThousandIsland is not meant to be a substitute for learning Prawn, you will get more out of it if you do. The excellent Prawn Manual is a great place to start.

Installation

Add this line to your application's Gemfile:

gem 'thousand_island'

And then execute:

$ bundle

Or install it yourself as:

$ gem install thousand_island

Usage

ThousandIsland uses the following classes to get the job done:

ThousandIsland::Template - The common layout and formatting for your pdfs live in here

ThousandIsland::Builder - Subclass the Builder for your actual business logic for the pdf itself

ThousandIsland::StyleSheet - A mixin for defining common styles to be used by your Template

A suggested directory structure for a Rails app is as follows, but it's entirely up to you:

app/
└── pdf_builders/
	├── my_builder.rb
    └── templates/
		├── my_template.rb  
		├── my_style_sheet.rb

For a non-Rails application, the lib directory can be used instead of the app directory, but it's all up to you.

Creating a Template

The Template class is where you can define elements that may be common to all (or some) documents within your application. It is likely that a common style will be required, so defining it in a Template and then using that Template subclass in any custom Builders DRYs up your pdf generation, as well as allowing for easy restyling across the whole application.

Typically, the Template subclass would define the settings for the Prawn Document, as well as the settings for the header and footer. Add your own or override any existing settings in the settings method. Any options passed into the constructor as a Hash will be merged with these settings, and the defaults.

Content for the header and footer will be defined in the methods header_content and footer_content. These methods are passed as a block when the pdf is rendered. Any standard Prawn methods may be used (including bounding boxes or any other layout tools). In addition, any of the styles from the StyleSheet can be applied as helper methods. For instance, the default style sheet has a h1_style method that returns a ThousandIsland::StyleHash, so in your code you can use:

  h1 "My Document Header"

and Prawn will render the text in the style set in the h1_style ThousandIsland::StyleHash. In addition to the supplied style methods, you can create a custom method:

  def magic_style
    ThousandIsland::StyleHash.new({
      size: 15
      style: bold
    })
  end

As long as the method ends in the word "_style" and returns a Hash, you magically get to do this:

  magic "My magic text is bold and size 15!!"

The method may return a standard Hash, but it is safer to return a ThousandIsland::StyleHash, as this dynamically duplicates a few keys to accommodate using the style in normal Prawn text methods as well as formatted text boxes, which use a slightly different convention. You don't have to worry about that if you use the ThousandIsland::StyleHash.

Alternatively, your method could do this:

  def magic_style
    h1_style.merge({
      size: 15
      style: bold
    })
  end

The following is an example of a custom template that subclasses ThousandIsland::Template -

  class MyTemplate < ThousandIsland::Template
    include MyCustomStyleSheet # optional

    # settings here are merged with and override the defaults
    def settings
      {
        header: {
          height: 55,
          render:true,
          repeated: true
        },
        footer: {
          render:true,
          height: 9,
          numbering_string: 'Page <page> of <total>',
          repeated: true
        }
      }
    end

    def header_content
      # Standard Prawn syntax
      pdf.image "#{pdf_images_path}/company_logo.png", height: 30
    end

    def footer_content
      # Using the magic method we get from the footer_style
      footer "www.mycompanyurl.com"
    end

    def pdf_images_path
      # How you go about this sort of thing is entirely up to you
	  "#{Rails.root}/app/assets/pdf_images"
    end
  end

Note: The Footer is a three column layout, with the numbering on the right column and the content defined here in the middle. More flexibility will be added in a later version.

Optional: Add a body_content method to add content before whatever the Builder defines in it's method of the same name.

StyleSheets

The StyleSheet is designed to be a mixin to the Template class. It may also be included into other modules to define custom StyleSheets.

Methods should return a StyleHash object rather than a vanilla Hash, as it has some customisation to help it work with Prawn. The default_style is used as the starting point for all other styles. For instance, the default_style[:size] value is multiplied in the heading styles, so changing the default style size value will have a cascading effect. Check the source for the default values and override as preferred.

An example of a custom StyleSheet:

  module MyStyleSheet
    include ThousandIsland::StyleSheet
    
    def default_style
      super.merge({
        size: 12,
        color: '222222'
      })
    end
    
    def h1_style
      super.merge({ align: :center })
    end
  end

Creating a Builder

Your Builder class is where you will put the necessary logic for rendering the final pdf. It's up to you how you get the data into the Builder. It will depend on the complexity. You might just pass an Invoice object (MyBuilder.new(invoice))) or you may have a bunch of methods that are called by an external object to get the data where it needs to be.

You must declare which Template class you will be using. Failing to do so will raise a TemplateRequiredError when you call the build method. Declare the template with the following in the main class body:

  uses_template MyTemplate

Your Builder can have a filename method, which will help a Rails Controller or other class determine the name to use to send the file to the browser or save to the filesystem (or both). Without this method it will have a default name, so you may choose to put the naming logic for your file elsewhere, it's up to you.

You must have a body_content method that takes no arguments (or the pdf will be empty!). This is the method that is passed around internally in order for Prawn to render what is in the method. You can use raw Prawn syntax, or any of the style magic methods to render to the pdf. You may also call other methods from your body_content method, and use Prawn syntax and magic methods in those too.

A Builder example might be:

  class MyBuilder < ThousandIsland::Builder
    uses_template MyTemplate
  
    attr_reader :data
    
    def initialize(data={})
      @data = data
      # do something with the data...
    end
    
    def filename
      "Document#{data.id_number}"
    end
    
    def body_content
      # call custom methods, magic methods or call Prawn methods directly:
      h1 'Main Heading'
      display_info
      body 'Main text in here...'
    end
    
    # Custom method called by body_content
    def display_info
      body "Written by: #{data.author}"
      pdf.image data.avatar, height: 20
    end
  end

Finally, to get the finished pdf from your Builder, call the build method like so:

  pdf = my_builder.build

Optional:

Define a header_content method to add content below whatever is defined in the Template. This will be repeated according to the header settings in the Template.

Define a footer_content method to add content above whatever is defined in the Template. This will be repeated according to the footer settings in the Template.

Define a settings method that returns a Hash. This will be passed to the Template class and will override any of the Template default settings.

Using the Builder in a Rails Application

Your Controller can look something like this:

def show
    @thing = Thing.find(params[:id])
    respond_to do |format|
      format.html
      format.pdf do
        data = { thing: @thing }  # How you structure this is up to you, it's your Class!!
        builder = MyBuilder.new(data)
        send_data builder.build, filename: builder.filename,
          type: "application/pdf",
          disposition: "inline" # Leave blank to render as a download
      end
    end
  end

If your controller for getting the data for a PDF is that simple, then you're pretty lucky. Normally we're going to want a PDF file to render a few things at once, so you might build a service object that formats the data, and use as follows:

  def show
    respond_to do |format|
      format.html do
        @thing_for_html_view = Thing.find(params[:id])
      end
      format.pdf do
      	# Tell the service object to do it's thing and return the Builder
        builder = ThingPdfServiceObject.new(params)
        send_data builder.build,
              filename: builder.filename,
              type: "application/pdf",
              disposition: "inline" # Leave blank to render as a download
      end
    end
  end

These are only suggestions, as you can probably tell there is nothing tying you down to a particular way of building and delivering the document. You might even want to save it to the file system, or upload to S3. You could override the build method if you wanted to:

  def build
  	pdf = super
    # Save, upload, send or do whatever....
  end

However, that kind of logic seems beyond the scope of the Builder, and should proabably be in the consumer of your Builder class, rather than the builder itself.

Tables

Quite often a pdf will need to render tabular data. It makes sense if there is a common table styling for all PDFs in your application (although it's not compulsory!). Thousand Island has a Table class, and a TableStyle class.

Note: Thousand Island has been designed to handle the most common scenarios, and therefore cannot cover every possibility and still keep it simple. If there's anything you can't do with a ThousandIsland::Table, you may need to use Prawn's standard table rendering. There's nothing stopping your Builder rendering a table without using any of the Table implementation in Thousand Island. Check out the Prawn Table Manual for the full capabilities.

Creating a Table

There are three main components of a table, header body and footer

Header

Header rows are optional. By doing nothing, your table will have no header row/s. To have a header in your table, you have several options:

  # Straightforward single row, no builder logic required
  header_rows = [['Column One', 'Column Two', 'Column Three']]
    
  # Build gradually (You can use Prawn native syntax for span and formatting)
  header_rows << [{content: 'First Two Columns', colspan: 2}, 'Third']
  header_rows << ['Column One', 'Column Two', 'Column Three']
    

By default Thousand Island takes all the rows in the header_rows and formats them as a repeating header. If you don't want a header, don't put anything in header_rows. If you don't want the header to repeat across pages, then set:

  header_repeat: false

in either the settings method (which returns a hash specific to this Table class) or by setting it in you TableSettings custom class.

Body

Similar to header_rows, body_rows can be set in one go or build gradually. Given the nature of the content of a table, it is more likely you'll have a method in that iterates through a collection of objects and builds the table according to the requirements. The way you build the Array of nested Array for the body is up to you. Three suggestions:

  • Simple Data: Simply set the body_rows as an Array of Arrays anywhere that's convenient
  • Slightly Complex: Iterate over a collection craeting and Array of "cellable" objects for Prawn, and add to body_rows as you go (body_rows << newly_built_row). You can do this in your Builder and pass it to the Table, or pass objects from your Builder to your Table so that the Table can deal with it.
  • Complex Real Life: Create a ServiceObject that knows everything it needs to know about how to build the rows. In some situations, a table row might need to represent multiple objects and the logic may be complex. Keeping the row building away from your Builder or Table classess keeps things simple.

Footer

Works in the same way as the header, except you use footer_rows. The only difference is that the footer does not repeat.

Table Options

A PrawnTable is very configurable. The goal of the Table component of Thousand Island is to make the configuration reusable throughout your application. There is a TableSettings class (see below) that you can subclass and use as a configuration object, but you can also override those settings for a specific table by setting a compatible Hash in your Table class like so:

  def settings
    {
      header_repeat: false
    }
  end

Table Settings

You can use the defaults, or create your own TableSettings class as below:

  class MyTableSettings < ThousandIsland::TableSettings
    def table_options
      {...}
    end
  end

All the settings in the table_options Hash will be merged with the defaults and made available to any Table that uses your custom TableSettings class.

To get one of your Table subclasses to use your custom TableSettings subclass, do the following:

  class MyTable < ThousandIsland::Table
    uses_settings MyTableSettings
    
    ...
    
  end

Using Tables in your Builder

Anywhere in your Builder, you can do:

  table = table_with MyTable

This initialises the Table class and gets it ready to build the body rows. You can then use it where appropriate to render the table in your PDF document:

  table.draw
  
  # To override any of the settins either in the table or the TableSettings being used, just pass them as a Hash to the draw method:
  table.draw(header_repeat: false)

Default Styles

Out of the box, ThousandIsland gives you some generic styles with default values. Override any of the values in your custom Stylesheet, or your Template. Create your own entirely new style in either of those places too, and get the magic method for free.

The default styles are:

body
{
  size: 10, # Inherited from default_style
  style: :normal, # Inherited from default_style
  align: :left, # Inherited from default_style
  leading: 1, # Inherited from default_style
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h1
{
  size: 18, # Calcuated as 1.8 * default_style[:size]
  style: :bold,
  align: :left, # Inherited from default_style
  leading: 8,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h2
{
  size: 15, # Calcuated as 1.5 * default_style[:size]
  style: :bold,
  align: :left, # Inherited from default_style
  leading: 4,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h3
{
  size: 14, # Calcuated as 1.4 * default_style[:size]
  style: :bold,
  align: :left, # Inherited from default_style
  leading: 4,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h4
{
  size: 11, # Calcuated as 1.1 * default_style[:size]
  style: :bold_italic,
  align: :left, # Inherited from default_style
  leading: 4,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h5
{
  size: 10, # Calcuated as 1 * default_style[:size]
  style: :normal, # Inherited from default_style
  align: :left, # Inherited from default_style
  leading: 4,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
h6
{
  size: 8.5, # Calcuated as 0.85 * default_style[:size]
  style: :italic,
  align: :left, # Inherited from default_style
  leading: 4,
  inline_format: true, # Inherited from default_style
  color: "000000" # Inherited from default_style
}
footer
{
  size: 0.8, # Calcuated as 0.8 * default_style[:size]
  style: :normal,
  align: :left, # Inherited from default_style
  leading: 1, # Inherited from default_style
  inline_format: true, # Inherited from default_style
  color: "666666"
}

Default Table Settings

Override any of these settings (or the nested settings) using the following techniques (in order of precedence):

  • Builder: table.draw(your_hash)
  • Table class: settings method that returns a hash
  • TableSettings class: table_settings method that returns a hash

The full list of defaults

{
  position: :center,
  width: pdf.bounds.width,   # Equates to full page width
  cell_style: {
    borders: [:top, :bottom],
    border_width: 0.5,
    inline_format: true,
    size: 10
  },
  header_repeat: true,
  header_format: {
    align: :center,
    font_style: :bold
  }
}

To come...

  • Easy list rendering and styling (including nested lists)
  • More flexibility in the Footer layout
  • (Possibly) Command line functions to create common subclass files

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request