Ruport’s Formatting System

There are three stages in the reporting process that Ruport can handle - collecting data, manipulating data and formatting. All three can be used independently, and in this article we’ll focusing exclusively on formatting. As a contrived example, we’ll run through the definition and use of a report listing book sales.

Renderer Definition

Generally, each report is made up of two or more classes – one for the definition (the renderer), and one for each desired output format (formatters). The renderer defines the stages and the data required to build the report – it is important to keep this data as format independent as possible. For our sales report, something like this might be appropriate:


  require 'rubygems'
  require 'ruport'

  class SalesReport < Ruport::Renderer
    required_option :titles
    option :report_title

    stage :document_header, :document_body, :document_footer      
    finalize :document
  end

This defines the following facts about our sales report:

This class should be a descendant of Ruport::Renderer, which defines a number of methods to help you create your definition. Data needed to build your report can be specified using the option or required_option methods. The stages involved in building your report can be specified using the prepare, stage, and finalize methods. As we’ll see, these define hooks that can be used in your format definitions.

Format Definition

On its own, the renderer listed above won’t do much – we need to tell Ruport how to render it into the required format.

To begin with, we will create a text version of the report. Something like the following placed immediately after the first class should work nicely:


  class SalesReportText < Ruport::Formatter
    renders :text, :for => SalesReport

    opt_reader :titles, :report_title

    def pad(str, len)
      return "".ljust(len) if str.nil?
      str = str.slice(0, len) # truncate long strings
      str.ljust(len) # pad with whitespace
    end

    def build_row(items, pads)
      items.each_with_index do |item, i|
        output << pad(item, pads[i]) << "|" 
      end
      output.chop!
      output << "\n" 
    end

    def build_document_header
      if report_title
        output << "".ljust(75,"*") << "\n" 
        output << "  #{report_title}\n" 
        output << "".ljust(75,"*") << "\n" 
        output << "\n" 
      end
    end

    def build_document_body
      # table heading
      build_row(['isbn', 'title', 'author', 'sales'], [15, 30, 15, 10])
      output << "".ljust(75,"#") << "\n" 

      # table data
      titles.each do |title|
        build_row([title["isbn"], title["title"], title["author"],
          title["sales"].to_s], [15, 30, 15, 10])
      end
      output << "".ljust(75,"#") << "\n" 
    end
  end

The first line of this class registers this output format with our renderer - this allows us to define as many different output formats for each renderer as we wish. The second line, the call to opt_reader, gives us attribute-like access to our options.

The pad function is a simple formatting function to simplify our work with strings and the build_row function is a helper to construct each row of the output.

The next two functions, build_document_header and build_document_body, are called to build the report. Notice the function names follow a particular style - these names are important and are a direct result of the stages defined by our renderer. They will be called in the order we specified in the renderer. The finalize_document function is also named to match the “finalize” line in the renderer.

Note that we don’t have to include all of the functions specified in the definition. In this case, we haven’t defined build_document_footer or finalize_document and if we don’t define a particular function, it simply won’t be called while building the report.

Using it

Now that our report is defined with at least one output format, we can use it in our application. One important thing to point out is that although Ruport contains its own Array-like class that makes managing your data easier, we haven’t used it in this example. Ruport’s Table class would be perfect for storing our book sales data, however we wanted to focus on building your report.

Assuming the report definition is in a file called salesreport.rb, the following code should be placed in app.rb in the same directory:


  require "salesreport" 

  book1 = { "isbn"   => "978111111111",
            "title"  => "Book Number One",
            "author" => "me",
            "sales"  => 10 }
  book2 = { "isbn"   => "978222222222",
            "title"  => "Two is better than one",
            "author" => "you",
            "sales"  => 267 }
  book3 = { "isbn"   => "978333333333",
            "title"  => "Three Blind Mice",
            "author" => "John Howard",
            "sales"  => 1 }
  book4 = { "isbn"   => "978444444444",
            "title"  => "The number 4",
            "author" => "George Bush",
            "sales"  => 1829 }

  books = [book1, book2, book3, book4]

  report = SalesReport.render_text do |e|
    e.report_title = "December Sales Figures" 
    e.titles = books
  end

  File.open("dec_sales.txt", "w") { |f| f.write report }

Once the sample data has been built, the report itself is generated with a single block. Using this approach, building the report within your app only requires a few simple lines, hiding all formatting complexity.

The output for this report is shown below

***************************************************************************
  December Sales Figures
***************************************************************************

isbn           |title                         |author         |sales     
###########################################################################
978111111111   |Book Number One               |me             |10        
978222222222   |Two is better than one        |you            |267       
978333333333   |Three Blind Mice              |John Howard    |1         
978444444444   |The number 4                  |George Bush    |1829      
###########################################################################	 

Adding PDF

Sure text is fine in many situations (e.g. emailing the report to a co-worker), but these days PDF is becoming the format of choice for many people. How do we add it as an option for our sales report?

As mentioned earlier, Ruport won’t try to abstract any of the complexities of formatting your report. The default library for generating PDFs in Ruport is PDF::Writer, and you will need to get your hands dirty with the foibles of this library to make your PDF. The following code placed inside salesreport.rb should get you started.


  class SalesReportPDF <  Ruport::Formatter::PDF
    renders :pdf, :for => SalesReport

    opt_reader :titles, :report_title

    def add_title( title )
      rounded_text_box("<b>#{title}</b>") do |o|
        o.fill_color = Color::RGB::Gray80
        o.radius     = 5  
        o.width      = options.header_width || 200
        o.height     = options.header_height || 20
        o.font_size  = options.header_font_size || 12
        o.x          = pdf_writer.absolute_right_margin - o.width 
        o.y          = pdf_writer.absolute_top_margin
      end
    end

    def build_document_header
      pad_bottom(50) { add_title(report_title) } if report_title 
    end

    def build_document_body 
      draw_table Table(:column_names => %w[isbn title author sales],
                       :data => titles), :maximum_width => 500
    end

    def finalize_document
      render_pdf
    end
  end

The structure of this is basically the same as the one that defined the text version, with two critical differences:

Ruport’s built-in formatting class does offer some methods to help you with your PDF formatting and we used a couple of them in our example above. The available methods include:

  • add_text – adds text to your output
  • center_image_in_box – takes an image path and box coordinates and centers within the box
  • rounded_text_box – draw text surrounded by a rounded-corner box
  • watermark – places a centered watermark on each page of your report
  • move_cursor – moves cursor specified number of units along the y-axis
  • move_cursor_to – moves cursor to a specified location on the y-axis
  • pad – adds a specified amount of space above and below some output
  • pad_top – adds a specified amount of space above some output
  • pad_bottom – adds a specified amount of space below some output
  • draw_table – uses PDF::SimpleTable to draw a table to output
  • horizontal_line – draw a horizontal line
  • vertical_line – draw a vertical line
  • left_boundary – get the left boundary of the page
  • right_boundary – get the right boundary of the page
  • top_boundary – get the top boundary of the page
  • bottom_boundary – get the bottom boundary of the page
  • cursor – get the current location of the cursor on the y-axis
  • draw_text – places text at a specified position on the page

So what changes do we have to make to our application to generate the PDF instead? Leave the sample data definition the same, just modify the remaining lines like so:


  report = SalesReport.render_pdf do |e|
    e.report_title = "December Sales Figures" 
    e.titles = books
  end

  File.open("dec_sales.pdf", "w") { |f| f.write report }

You can take a look at the pretty output here

Switching output formats within your app according to user preference or whatever is a piece of cake.