class Prawn::Table

This class implements simple PDF table generation.

Prawn tables have the following features:

* Can be generated with or without headers
* Can tweak horizontal and vertical padding of text
* Minimal styling support (borders / row background colors)
* Can be positioned by bounding boxes (left/center aligned) or an
  absolute x position
* Automated page-breaking as needed
* Column widths can be calculated automatically or defined explictly on a 
  column by column basis
* Text alignment can be set for the whole table or by column

The current implementation is a bit barebones, but covers most of the basic needs for PDF table generation. If you have feature requests, please share them at: groups.google.com/group/prawn-ruby

Tables will be revisited before the end of the Ruby Mendicant project and the most commonly needed functionality will likely be added.

Public Class Methods

new(data, document, options={}) click to toggle source

Creates a new Document::Table object. This is generally called indirectly through Prawn::Document#table but can also be used explictly.

The data argument is a two dimensional array of strings, organized by row, e.g. [[“r1-col1”,“r1-col2”],]. As with all Prawn text drawing operations, strings must be UTF-8 encoded.

The following options are available for customizing your tables, with defaults shown in [] at the end of each description.

:headers

An array of table headers, either strings or Cells. [Empty]

:align_headers

Alignment of header text. Specify for entire header (:left) or by column ({ 0 => :right, 1 => :left}). If omitted, the header alignment is the same as the column alignment.

:header_text_color

Sets the text color of the headers

:header_color

Manually sets the header color

:font_size

The font size for the text cells . [12]

:horizontal_padding

The horizontal cell padding in PDF points [5]

:vertical_padding

The vertical cell padding in PDF points [5]

:padding

Horizontal and vertical cell padding (overrides both)

:border_width

With of border lines in PDF points [1]

:border_style

If set to :grid, fills in all borders. If set to :underline_header, underline header only. Otherwise, borders are drawn on columns only, not rows

:border_color

Sets the color of the borders.

:position

One of :left, :center or n, where n is an x-offset from the left edge of the current bounding box

:width

A set width for the table, defaults to the sum of all column widths

:column_widths

A hash of indices and widths in PDF points. E.g. { 0 => 50, 1 => 100 }

:row_colors

Used to specify background colors for rows. See below for usage.

:align

Alignment of text in columns, for entire table (:center) or by column ({ 0 => :left, 1 => :center})

Row colors (:row_colors) are specified as HTML hex color values, e.g., “ccaaff”. They can take several forms:

  • An array of colors, used cyclically to “zebra stripe” the table: ['ffffff', 'cccccc', '336699'].

  • A hash taking 0-based row numbers to colors: { 0 => 'ffffff', 2 => 'cccccc'}.

  • The symbol :pdf_writer, for PDF::Writer's default color scheme.

See Prawn::Document#table for typical usage, as directly using this class is not recommended unless you know why you want to do it.

# File lib/prawn/table.rb, line 121
def initialize(data, document, options={})     
  unless data.all? { |e| Array === e }
    raise Prawn::Errors::InvalidTableData,
      "data must be a two dimensional array of Prawn::Cells or strings"
  end
  
  @data     = data        
  @document = document
  
  Prawn.verify_options [:font_size,:border_style, :border_width,
   :position, :headers, :row_colors, :align, :align_headers, 
   :header_text_color, :border_color, :horizontal_padding, 
   :vertical_padding, :padding, :column_widths, :width, :header_color ], 
   options     
                                      
  configuration.update(options)  

  if padding = options[:padding]
    C(:horizontal_padding => padding, :vertical_padding => padding) 
  end
   
  if options[:row_colors] == :pdf_writer 
    C(:row_colors => ["ffffff","cccccc"])  
  end
  
  if options[:row_colors]
    C(:original_row_colors => C(:row_colors)) 
  end

  calculate_column_widths(options[:column_widths], options[:width])
end

Public Instance Methods

draw() click to toggle source

Draws the table onto the PDF document

# File lib/prawn/table.rb, line 163
def draw  
  @parent_bounds = @document.bounds  
  case C(:position) 
  when :center
    x = (@document.bounds.width - width) / 2.0
    dy = @document.bounds.absolute_top - @document.y
    final_pos = nil

    @document.bounding_box [x, @parent_bounds.top], :width => width do 
      @document.move_down(dy)
      generate_table
      final_pos = @document.y
    end

    @document.y = final_pos
  when Numeric     
    x, y = C(:position), @document.cursor
    final_pos = nil

    @document.bounding_box([x,y], :width => width) do
      generate_table
      final_pos = @document.y 
    end

    @document.y = final_pos
  else
    generate_table
  end
end
width() click to toggle source

Width of the table in PDF points

# File lib/prawn/table.rb, line 157
def width
   @column_widths.inject(0) { |s,r| s + r }
end

Private Instance Methods

calculate_column_widths(manual_widths=nil, width=nil) click to toggle source
# File lib/prawn/table.rb, line 203
def calculate_column_widths(manual_widths=nil, width=nil)
  @column_widths = [0] * @data[0].inject(0){ |acc, e| 
    acc += (e.is_a?(Hash) && e.has_key?(:colspan)) ? e[:colspan] : 1 }

  renderable_data.each do |row|
    colspan = 0
    row.each_with_index do |cell,i|
      cell_text = cell.is_a?(Hash) ? cell[:text] : cell.to_s
      length = cell_text.lines.map { |e|
        @document.width_of(e, :size => C(:font_size)) }.max.to_f +
          2*C(:horizontal_padding)
      if length > @column_widths[i+colspan]
        @column_widths[i+colspan] = length.ceil
      end

      if cell.is_a?(Hash) && cell[:colspan]
        colspan += cell[:colspan] - 1
      end
    end
  end  

  fit_within_bounds(manual_widths, width)
end
default_configuration() click to toggle source
# File lib/prawn/table.rb, line 195
def default_configuration     
  { :font_size           => 12, 
    :border_width        => 1, 
    :position            => :left,
    :horizontal_padding  => 5,
    :vertical_padding    => 5 } 
end
draw_page(contents) click to toggle source
# File lib/prawn/table.rb, line 360
def draw_page(contents)
  return if contents.empty?

  if C(:border_style) == :underline_header
    contents.each { |e| e.border_style = :none }
    contents.first.border_style = :bottom_only if C(:headers)
  elsif C(:border_style) == :grid || contents.length == 1
    contents.each { |e| e.border_style = :all }
  else
    contents.first.border_style = C(:headers) ? :all : :no_bottom
    contents.last.border_style = :no_top
  end
  
  if C(:headers)
    contents.first.cells.each_with_index do |e,i|
      if C(:align_headers)
        case C(:align_headers)
          when Hash
            align = C(:align_headers)[i]
          else
            align = C(:align_headers)
          end
      end
      e.align = align if align
      e.text_color = C(:header_text_color) if C(:header_text_color)
      e.background_color = C(:header_color) if C(:header_color)
    end
  end
  
  contents.each do |x|
    unless x.background_color
      x.background_color = next_row_color if C(:row_colors)
    end
    x.border_color = C(:border_color) if C(:border_color)
    
    x.draw 
  end

  reset_row_colors
end
fit_within_bounds(manual_widths, width) click to toggle source
# File lib/prawn/table.rb, line 227
def fit_within_bounds(manual_widths, width)
  manual_width = 0
  manual_widths.each { |k,v| 
    @column_widths[k] = v; manual_width += v } if manual_widths           

  #Ensures that the maximum width of the document is not exceeded
  #Takes into consideration the manual widths specified (With full manual 
  # widths specified, the width can exceed the document width as manual 
  # widths are taken as gospel)
  max_width = width || @document.margin_box.width
  calculated_width = @column_widths.inject {|sum,e| sum += e }

  if calculated_width > max_width
    shrink_by = (max_width - manual_width).to_f / 
      (calculated_width - manual_width)
    @column_widths.each_with_index { |c,i| 
      @column_widths[i] = c * shrink_by if manual_widths.nil? || 
        manual_widths[i].nil? 
    }
  elsif width && calculated_width < width
    grow_by = (width - manual_width).to_f / 
      (calculated_width - manual_width)
    @column_widths.each_with_index { |c,i| 
      @column_widths[i] = c * grow_by if manual_widths.nil? || 
        manual_widths[i].nil? 
    }
  end
end
generate_table() click to toggle source
# File lib/prawn/table.rb, line 261
def generate_table    
  page_contents = []
  y_pos = @document.y 

  @document.font_size C(:font_size) do
    renderable_data.each_with_index do |row,index|
      c = Prawn::Table::CellBlock.new(@document)
      
      if C(:row_colors).is_a?(Hash)
        real_index = index
        real_index -= 1 if C(:headers)
        color = C(:row_colors)[real_index]
        c.background_color = color if color
      end
      
      col_index = 0
      row.each do |e|
        case C(:align)
        when Hash
          align            = C(:align)[col_index]
        else
          align            = C(:align)
        end   
        
        
        align ||= e.to_s =~ NUMBER_PATTERN ? :right : :left 
        
        case e
        when Prawn::Table::Cell
          e.document = @document
          e.width    = @column_widths[col_index]
          e.horizontal_padding = C(:horizontal_padding)
          e.vertical_padding   = C(:vertical_padding)    
          e.border_width       = C(:border_width)
          e.border_style       = :sides
          e.align              = align 
          c << e
        else
          text = e.is_a?(Hash) ? e[:text] : e.to_s
          width = if e.is_a?(Hash) && e.has_key?(:colspan)
            @column_widths.slice(col_index, e[:colspan]).inject { 
              |sum, width| sum + width }
          else
            @column_widths[col_index]
          end
          
          cell_options = {
            :document => @document, 
            :text     => text,
            :width    => width,
            :horizontal_padding => C(:horizontal_padding),
            :vertical_padding   => C(:vertical_padding),
            :border_width       => C(:border_width),
            :border_style       => :sides,
            :align              => align 
          }

          if e.is_a?(Hash) 
            opts = e.dup
            opts.delete(:colspan)
            cell_options.update(opts)
          end

          c << Prawn::Table::Cell.new(cell_options)
        end
        
        col_index += (e.is_a?(Hash) && e.has_key?(:colspan)) ? e[:colspan] : 1
      end
                                          
      bbox = @parent_bounds.stretchy? ? @document.margin_box : @parent_bounds
      if c.height > y_pos - bbox.absolute_bottom
        if C(:headers) && page_contents.length == 1
          @parent_bounds.move_past_bottom
          y_pos = @document.y
        else
          draw_page(page_contents)
          @parent_bounds.move_past_bottom
          if C(:headers) && page_contents.any?
            page_contents = [page_contents[0]]
            y_pos = @document.y - page_contents[0].height
          else
            page_contents = []
            y_pos = @document.y
          end
        end
      end

      page_contents << c

      y_pos -= c.height

      if index == renderable_data.length - 1
        draw_page(page_contents)
      end

    end
  end
end
next_row_color() click to toggle source
# File lib/prawn/table.rb, line 402
def next_row_color
  return if C(:row_colors).is_a?(Hash)

  color = C(:row_colors).shift
  C(:row_colors).push(color)
  color
end
renderable_data() click to toggle source
# File lib/prawn/table.rb, line 257
def renderable_data
  C(:headers) ? [C(:headers)] + @data : @data
end
reset_row_colors() click to toggle source
# File lib/prawn/table.rb, line 410
def reset_row_colors    
  C(:row_colors => C(:original_row_colors).dup) if C(:row_colors)
end