# encoding: utf-8 # # table.rb : Simple table drawing functionality # # Copyright June 2008, Gregory Brown. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. require "prawn/table/cell" module Prawn class Document # Builds and renders a Document::Table object from raw data. # For details on the options that can be passed, see # Document::Table.new # # data = [["Gregory","Brown"],["James","Healy"],["Jia","Wu"]] # # Prawn::Document.generate("table.pdf") do # # # Default table, without headers # table(data) # # # Default table with headers # table data, :headers => ["First Name", "Last Name"] # # # Very close to PDF::Writer's default SimpleTable output # table data, :headers => ["First Name", "Last Name"], # :font_size => 10, # :vertical_padding => 2, # :horizontal_padding => 5, # :position => :center, # :row_colors => :pdf_writer, # # # Grid border style with explicit column widths. # table data, :border_style => :grid, # :column_widths => { 0 => 100, 1 => 150 } # # end # # Will raise Prawn::Errors::EmptyTable given # a nil or empty data paramater. # def table(data, options={}) if data.nil? || data.empty? raise Prawn::Errors::EmptyTable, "data must be a non-empty, non-nil, two dimensional array " + "of Prawn::Cells or strings" end Prawn::Table.new(data,self,options).draw end end # 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: http://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. # class Table include Prawn::Configurable attr_reader :column_widths # :nodoc: NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?$/ #:nodoc: # Creates a new Document::Table object. This is generally called # indirectly through 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"],["r2-col2","r2-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 Document#table for typical usage, as directly using this class is # not recommended unless you know why you want to do it. # 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 attr_reader :column_widths #:nodoc: # Width of the table in PDF points # def width @column_widths.inject(0) { |s,r| s + r } end # Draws the table onto the PDF document # 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 private def default_configuration { :font_size => 12, :border_width => 1, :position => :left, :horizontal_padding => 5, :vertical_padding => 5 } end 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 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 def renderable_data C(:headers) ? [C(:headers)] + @data : @data end 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 @document.start_new_page y_pos = @document.y else draw_page(page_contents) @document.start_new_page 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 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 def next_row_color return if C(:row_colors).is_a?(Hash) color = C(:row_colors).shift C(:row_colors).push(color) color end def reset_row_colors C(:row_colors => C(:original_row_colors).dup) if C(:row_colors) end end end