prawn-flexible-table 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates various table and cell features.
4
+ #
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
6
+
7
+ require "rubygems"
8
+ require "prawn"
9
+ require "prawn/layout"
10
+
11
+ headers, body = nil, nil
12
+
13
+ dir = File.expand_path(File.dirname(__FILE__))
14
+
15
+ ruby_18 do
16
+ require "fastercsv"
17
+ headers, *body = FasterCSV.read("#{dir}/addressbook.csv")
18
+ end
19
+
20
+ ruby_19 do
21
+ require "csv"
22
+ headers, *body = CSV.read("#{dir}/addressbook.csv",
23
+ :encoding => "utf-8")
24
+ end
25
+
26
+ Prawn::Document.generate("fancy_table.pdf", :page_layout => :landscape) do
27
+
28
+ #font "#{Prawn::BASEDIR}/data/fonts/DejaVuSans.ttf"
29
+
30
+ mask(:y) { table body, :headers => headers,
31
+ :align => :center,
32
+ :border_style => :grid }
33
+
34
+ table [["This is", "A Test" ],
35
+ [ Prawn::Table::Cell.new( :text => "Of tables",
36
+ :background_color => "ffccff" ),
37
+ "Drawn Side"], ["By side", "and stuff" ]],
38
+ :position => 600,
39
+ :headers => ["Col A", "Col B"],
40
+ :border_width => 1,
41
+ :vertical_padding => 5,
42
+ :horizontal_padding => 3,
43
+ :font_size => 10,
44
+ :row_colors => :pdf_writer,
45
+ :column_widths => { 1 => 50 }
46
+
47
+ move_down 150
48
+
49
+ table [%w[1 2 3],%w[4 5 6],%w[7 8 9]],
50
+ :position => :center,
51
+ :border_width => 0,
52
+ :font_size => 40
53
+
54
+ cell [500,300],
55
+ :text => "This free flowing textbox shows how you can use Prawn's "+
56
+ "cells outside of a table with ease. Think of a 'cell' as " +
57
+ "simply a limited purpose bounding box that is meant for laying " +
58
+ "out blocks of text and optionally placing a border around it",
59
+ :width => 225, :padding => 10, :border_width => 2
60
+
61
+ font_size 24
62
+
63
+ cell [50,75],
64
+ :text => "This document demonstrates a number of Prawn's table features",
65
+ :border_style => :no_top, # :all, :no_bottom, :sides
66
+ :horizontal_padding => 5
67
+ end
data/examples/table.rb ADDED
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Generates a couple simple tables, including some UTF-8 text cells.
4
+ # Although this does not show all of the options available to table, the most
5
+ # common are used here. See fancy_table.rb for a more comprehensive example.
6
+ #
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
8
+
9
+ require "rubygems"
10
+ require "prawn"
11
+ require "prawn/layout"
12
+
13
+ Prawn::Document.generate("table.pdf") do
14
+ font "#{Prawn::BASEDIR}/data/fonts/DejaVuSans.ttf"
15
+ table [["ὕαλον ϕαγεῖν", "baaar", "1" ],
16
+ ["This is","a sample", "2" ],
17
+ ["Table", "dont\ncha\nknow?", "3" ],
18
+ [ "It", "Rules", "4" ],
19
+ [ "It", "Rules", "4" ],
20
+ [ "It", "Rules", "4123231" ],
21
+ [ "It", "Rules", "22.5" ],
22
+ [ "It", "Rules", "4" ],
23
+ [ "It", "Rules", "4" ],
24
+ [ "It", "Rules", "4" ],
25
+ [ "It", "Rules", "4" ],
26
+ [ "It", "Rules", "4" ],
27
+ [ "It", "Rules\nwith an iron fist", "x" ],
28
+ [ "It", "Rules", "4" ],
29
+ [ "It", "Rules", "4" ],
30
+ [ "It", "Rules", "4" ],
31
+ [ "It", "Rules", "4" ],
32
+ [ "It", "Rules", "4" ],
33
+ [ "It", "Rules", "4" ],
34
+ [ "It", "Rules", "4" ],
35
+ [ "It", "Rules", "4" ],
36
+ [ "It", "Rules", "4" ]],
37
+
38
+ :font_size => 24,
39
+ :horizontal_padding => 10,
40
+ :vertical_padding => 3,
41
+ :border_width => 2,
42
+ :position => :center,
43
+ :headers => ["Column A","Column B","#"],
44
+ :align => {1 => :center},
45
+ :align_headers => :center
46
+
47
+ text "This should appear in the original font size, just below the table"
48
+ move_down 10
49
+
50
+ table [[ "Wide", "columns", "streeetch"],
51
+ ["are","mighty fine", "streeeeeeeech"]],
52
+ :column_widths => { 0 => 200, 1 => 200 }, :position => 5
53
+
54
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates the many controls over alignment and positioning in Prawn
4
+ # tables.
5
+ #
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
7
+
8
+ require "rubygems"
9
+ require "prawn"
10
+ require "prawn/layout"
11
+
12
+ Prawn::Document.generate "table_header_align.pdf" do
13
+ table [ ['01/01/2008', 'John Doe', '4.2', '125.00', '525.00'],
14
+ ['01/12/2008', 'Jane Doe', '3.2', '75.50', '241.60'] ] * 20,
15
+ :position => :center,
16
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
17
+ :column_widths => { 0 => 75, 1 => 100, 2 => 50, 3 => 50, 4 => 50},
18
+ :border_style => :grid,
19
+ :align => { 0 => :right, 1 => :left, 2 => :right, 3 => :right, 4 => :right },
20
+ :align_headers => { 0 => :center, 2 => :left, 3 => :left, 4 => :right }
21
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates how to set the table border color with the :border_color
4
+ # attribute.
5
+ #
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
7
+
8
+ require "rubygems"
9
+ require "prawn"
10
+ require "prawn/layout"
11
+
12
+ Prawn::Document.generate "table_border_color.pdf" do
13
+ table [ ['01/01/2008', 'John Doe', '4.2', '125.00', '525.00'],
14
+ ['01/12/2008', 'Jane Doe', '3.2', '75.50', '241.60'] ] * 20,
15
+ :position => :center,
16
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
17
+ :column_widths => { 0 => 75, 1 => 100, 2 => 50, 3 => 50, 4 => 50},
18
+ :border_style => :grid,
19
+ :border_color => "ff0000"
20
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates the use of the :col_span option when using Document#table
4
+ #
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
6
+
7
+ require "rubygems"
8
+ require "prawn"
9
+ require "prawn/layout"
10
+
11
+ Prawn::Document.generate "table_colspan.pdf" do
12
+ data = [ ['01/01/2008', 'John Doe', '4.2', '125.00', '525.00'],
13
+ ['01/12/2008', 'Jane Doe', '3.2', '75.50', '241.60'] ] * 5
14
+
15
+ data << [{:text => 'Total', :colspan => 2}, '37.0', '1002.5', '3833']
16
+
17
+ table data,
18
+ :position => :center,
19
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
20
+ :column_widths => { 0 => 75, 1 => 100, 2 => 50, 3 => 50, 4 => 50},
21
+ :border_style => :grid
22
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates explicitly setting the :header_color rather than inferring
4
+ # it from :row_colors in Document#table
5
+ #
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
7
+
8
+ require "rubygems"
9
+ require "prawn"
10
+ require "prawn/layout"
11
+
12
+ Prawn::Document.generate "table_header_color.pdf" do
13
+ table [ ['01/01/2008', 'John Doe', '4.2', '125.00', '525.00'],
14
+ ['01/12/2008', 'Jane Doe', '3.2', '75.50', '241.60'] ] * 20,
15
+ :position => :center,
16
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
17
+ :column_widths => { 0 => 75, 1 => 100, 2 => 50, 3 => 50, 4 => 50},
18
+ :border_style => :grid,
19
+ :header_color => 'f07878',
20
+ :header_text_color => "990000",
21
+ :row_colors => ["FFCCFF","CCFFCC"]
22
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates the :underline_header border style for Document#table.
4
+ #
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
6
+
7
+ require "rubygems"
8
+ require "prawn"
9
+ require "prawn/layout"
10
+
11
+ Prawn::Document.generate "table_header_underline.pdf" do
12
+ table [ ['01/01/2008', 'John Doe', '4.2', '125.00', '525.00'],
13
+ ['01/12/2008', 'Jane Doe', '3.2', '75.50', '241.60'] ] * 5,
14
+ :position => :center,
15
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
16
+ :column_widths => { 0 => 75, 1 => 100, 2 => 50, 3 => 50, 4 => 50},
17
+ :border_style => :underline_header
18
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Demonstrates the use of the :rowspan option when using Document#table
4
+ #
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
6
+
7
+ require "rubygems"
8
+ gem 'prawn-core'
9
+ require "prawn"
10
+ require "prawn/layout"
11
+
12
+ Prawn::Document.generate "table_rowspan.pdf" do
13
+ data = [ [ { :rowspan => 10, :text => '01/01/2008' },
14
+ { :rowspan => 5, :text => 'John Doe' },
15
+ '4.2', '125.00', '525.00' ],
16
+ [ '4.2', '125.00', '525.00' ],
17
+ [ '4.2', '125.00', '525.00' ],
18
+ [ '4.2', '125.00', '525.00' ],
19
+ [ '4.2', '125.00', '525.00' ],
20
+ [ Prawn::Table::Cell.new( :rowspan => 5, :text => 'Jane Doe' ),
21
+ '3.2', '75.50', '241.60' ],
22
+ [ '3.2', '75.50', '241.60' ],
23
+ [ '3.2', '75.50', '241.60' ],
24
+ [ '3.2', '75.50', '241.60' ],
25
+ [ '3.2', '75.50', '241.60' ] ]
26
+
27
+ data << [{:text => 'Total', :colspan => 2}, '37.0', '1002.5', '3833']
28
+
29
+ table data,
30
+ :position => :center,
31
+ :headers => ['Date', 'Employee', 'Hours', 'Rate', 'Total'],
32
+ :border_style => :grid
33
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Generates a couple simple tables, including some UTF-8 text cells.
4
+ # Although this does not show all of the options available to table, the most
5
+ # common are used here. See fancy_table.rb for a more comprehensive example.
6
+ #
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
8
+
9
+ require "rubygems"
10
+ require "prawn"
11
+ require "prawn/layout"
12
+
13
+ Prawn::Document.generate("table_widths.pdf") do
14
+
15
+ data = [
16
+ %w(one two three four),
17
+ %w(five six seven eight),
18
+ %w(nine ten eleven twelve),
19
+ %w(thirteen fourteen fifteen sixteen),
20
+ %w(seventeen eighteen nineteen twenty)
21
+ ]
22
+ headers = ["Column A","Column B","Column C", "Column D"]
23
+
24
+ text "A table with a specified width of the document width (within margins)"
25
+ move_down 10
26
+
27
+ table data,
28
+ :position => :center,
29
+ :headers => headers,
30
+ :width => margin_box.width
31
+
32
+
33
+ move_down 20
34
+ text "A table with a specified width of the document width (within margins) and two fixed width columns"
35
+ move_down 10
36
+
37
+ table data,
38
+ :position => :center,
39
+ :headers => headers,
40
+ :width => margin_box.width,
41
+ :column_widths => {0 => 70, 1 => 70}
42
+
43
+
44
+ move_down 20
45
+ text "A table with a specified width of 300"
46
+ move_down 10
47
+
48
+ table data,
49
+ :position => :center,
50
+ :headers => headers,
51
+ :width => 300
52
+
53
+
54
+ move_down 20
55
+ text "A table with too much data is automatically limited to the document width"
56
+ move_down 10
57
+
58
+ data << ['some text', 'A long piece of text that will make this cell too wide for the page', 'some more text', 'And more text']
59
+
60
+ table data,
61
+ :position => :center,
62
+ :headers => headers
63
+
64
+ end
@@ -0,0 +1,9 @@
1
+ module Prawn; end
2
+
3
+ module Prawn::Errors
4
+ # This error is raised when table data is malformed
5
+ InvalidTableData = Class.new(StandardError)
6
+
7
+ # This error is raised when an empty or nil table is rendered
8
+ EmptyTable = Class.new(StandardError)
9
+ end
@@ -0,0 +1,516 @@
1
+ require "prawn/flexible-table/cell"
2
+ require "prawn/errors"
3
+
4
+ module Prawn; end
5
+
6
+ class Prawn::Document
7
+
8
+ # Builds and renders a Document::FlexibleTable object from raw data.
9
+ # For details on the options that can be passed, see
10
+ # Document::FlexibleTable.new
11
+ #
12
+ # data = [["Gregory","Brown"],["James","Healy"],["Jia","Wu"]]
13
+ #
14
+ # Prawn::Document.generate("table.pdf") do
15
+ #
16
+ # # Default table, without headers
17
+ # flexible_table(data)
18
+ #
19
+ # # Default flexible table with headers
20
+ # flexible_table data, :headers => ["First Name", "Last Name"]
21
+ #
22
+ # # Very close to PDF::Writer's default SimpleTable output
23
+ # flexible_table data,
24
+ # :headers => ["First Name", "Last Name"],
25
+ # :font_size => 10,
26
+ # :vertical_padding => 2,
27
+ # :horizontal_padding => 5,
28
+ # :position => :center,
29
+ # :row_colors => :pdf_writer,
30
+ #
31
+ # # Grid border style with explicit column widths.
32
+ # flexible_table data,
33
+ # :border_style => :grid,
34
+ # :column_widths => { 0 => 100, 1 => 150 }
35
+ #
36
+ # end
37
+ #
38
+ # Will raise <tt>Prawn::Errors::EmptyTable</tt> given
39
+ # a nil or empty <tt>data</tt> paramater.
40
+ #
41
+ def flexible_table(data, options={})
42
+ if data.nil? || data.empty?
43
+ raise Prawn::Errors::EmptyTable,
44
+ "data must be a non-empty, non-nil, two dimensional array of Prawn::Cells or strings"
45
+ end
46
+ Prawn::Table.new(data,self,options).draw
47
+ end
48
+ end
49
+
50
+
51
+ # This class implements simple PDF flexible table generation.
52
+ #
53
+ # Prawn tables have the following features:
54
+ #
55
+ # * Can be generated with or without headers
56
+ # * Can tweak horizontal and vertical padding of text
57
+ # * Minimal styling support (borders / row background colors)
58
+ # * Can be positioned by bounding boxes (left/center aligned) or an
59
+ # absolute x position
60
+ # * Automated page-breaking as needed
61
+ # * Column widths can be calculated automatically or defined explictly on a
62
+ # column by column basis
63
+ # * Text alignment can be set for the whole table or by column
64
+ # * Cells can have both rowspan and colspan attributes
65
+ #
66
+ # The current implementation is a bit barebones, but covers most of the
67
+ # basic needs for PDF table generation. If you have feature requests,
68
+ # please share them at: http://groups.google.com/group/prawn-ruby
69
+ #
70
+ class Prawn::FlexibleTable
71
+
72
+ include Prawn::Configurable
73
+
74
+ attr_reader :column_widths # :nodoc:
75
+
76
+ NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?$/ #:nodoc:
77
+
78
+ # Creates a new Document::FlexibleTable object. This is generally called
79
+ # indirectly through Document#flexible_table but can also be used explictly.
80
+ #
81
+ # The <tt>data</tt> argument is a two dimensional array of either string,
82
+ # FlexibleTable::Cell or hashes (with the options to create a Cell object),
83
+ # organized by row, e.g.:
84
+ #
85
+ # [["r1-col1","r1-col2"],["r2-col2","r2-col2"]]
86
+ #
87
+ # [ [ {:text => "r1-2 col1-2", :rowspan => 2, :colspan => 2}, "r1-col3"],
88
+ # [ {:text => "r2 col 3", :text_color => "EEAAFF" } ],
89
+ # [ "r3 col1", "r3 col2", "r3 col3" ] ]
90
+ #
91
+ #
92
+ # As with all Prawn text drawing operations, strings must be UTF-8 encoded.
93
+ #
94
+ # The following options are available for customizing your tables, with
95
+ # defaults shown in [] at the end of each description.
96
+ #
97
+ # <tt>:headers</tt>:: An array of table headers, either strings or Cells. [Empty]
98
+ # <tt>:align_headers</tt>:: Alignment of header text. Specify for entire header (<tt>:left</tt>) or by column (<tt>{ 0 => :right, 1 => :left}</tt>). If omitted, the header alignment is the same as the column alignment.
99
+ # <tt>:header_text_color</tt>:: Sets the text color of the headers
100
+ # <tt>:header_color</tt>:: Manually sets the header color
101
+ # <tt>:font_size</tt>:: The font size for the text cells . [12]
102
+ # <tt>:horizontal_padding</tt>:: The horizontal cell padding in PDF points [5]
103
+ # <tt>:vertical_padding</tt>:: The vertical cell padding in PDF points [5]
104
+ # <tt>:padding</tt>:: Horizontal and vertical cell padding (overrides both)
105
+ # <tt>:border_width</tt>:: With of border lines in PDF points [1]
106
+ # <tt>:border_style</tt>:: 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
107
+ # <tt>:border_color</tt>:: Sets the color of the borders.
108
+ # <tt>:position</tt>:: One of <tt>:left</tt>, <tt>:center</tt> or <tt>n</tt>, where <tt>n</tt> is an x-offset from the left edge of the current bounding box
109
+ # <tt>:width:</tt>:: A set width for the table, defaults to the sum of all column widths
110
+ # <tt>:column_widths:</tt>:: A hash of indices and widths in PDF points. E.g. <tt>{ 0 => 50, 1 => 100 }</tt>
111
+ # <tt>:row_colors</tt>:: An array of row background colors which are used cyclicly.
112
+ # <tt>:align</tt>:: Alignment of text in columns, for entire table (<tt>:center</tt>) or by column (<tt>{ 0 => :left, 1 => :center}</tt>)
113
+ #
114
+ # Row colors are specified as html encoded values, e.g.
115
+ # ["ffffff","aaaaaa","ccaaff"]. You can also specify
116
+ # <tt>:row_colors => :pdf_writer</tt> if you wish to use the default color
117
+ # scheme from the PDF::Writer library.
118
+ #
119
+ # See Document#flexible_table for typical usage, as directly using this class is
120
+ # not recommended unless you know why you want to do it.
121
+ #
122
+ def initialize(data, document, options={})
123
+ unless data.all? { |e| Array === e }
124
+ raise Prawn::Errors::InvalidTableData,
125
+ "data must be a two dimensional array of Prawn::Cells or strings"
126
+ end
127
+
128
+ @data = data
129
+ @document = document
130
+
131
+ Prawn.verify_options [:font_size,:border_style, :border_width,
132
+ :position, :headers, :row_colors, :align, :align_headers,
133
+ :header_text_color, :border_color, :horizontal_padding,
134
+ :vertical_padding, :padding, :column_widths, :width, :header_color ],
135
+ options
136
+
137
+ configuration.update(options)
138
+
139
+ if padding = options[:padding]
140
+ C(:horizontal_padding => padding, :vertical_padding => padding)
141
+ end
142
+
143
+ if options[:row_colors] == :pdf_writer
144
+ C(:row_colors => ["ffffff","cccccc"])
145
+ end
146
+
147
+ if options[:row_colors]
148
+ C(:original_row_colors => C(:row_colors))
149
+ end
150
+
151
+ # Once we have all configuration setted...
152
+ normalize_data
153
+ check_rows_lengths
154
+ calculate_column_widths(options[:column_widths], options[:width])
155
+ end
156
+
157
+ attr_reader :column_widths #:nodoc:
158
+
159
+ # Width of the table in PDF points
160
+ #
161
+ def width
162
+ @column_widths.inject(0) { |s,r| s + r }
163
+ end
164
+
165
+ # Draws the table onto the PDF document
166
+ #
167
+ def draw
168
+ @parent_bounds = @document.bounds
169
+ case C(:position)
170
+ when :center
171
+ x = (@document.bounds.width - width) / 2.0
172
+ dy = @document.bounds.absolute_top - @document.y
173
+ @document.bounding_box [x, @parent_bounds.top], :width => width do
174
+ @document.move_down(dy)
175
+ generate_table
176
+ end
177
+ when Numeric
178
+ x, y = C(:position), @document.y - @document.bounds.absolute_bottom
179
+ @document.bounding_box([x,y], :width => width) { generate_table }
180
+ else
181
+ generate_table
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def default_configuration
188
+ { :font_size => 12,
189
+ :border_width => 1,
190
+ :position => :left,
191
+ :horizontal_padding => 5,
192
+ :vertical_padding => 5 }
193
+ end
194
+
195
+ # Check that all rows are well formed with the same length.
196
+ #
197
+ # Will raise an <tt>Prawn::Errors::InvalidTableData</tt> exception
198
+ # in case that a bad formed row is found
199
+ def check_rows_lengths
200
+ tables_width = nil
201
+ actual_row = 0
202
+ old_index = -1
203
+ check_last_row = lambda {
204
+ tables_width ||= old_index # only setted the first time
205
+ if tables_width != nil && tables_width != old_index
206
+ raise Prawn::Errors::InvalidTableData,
207
+ "The row #{actual_row} has a length of #{old_index + 1}, " +
208
+ "it should be of #{tables_width + 1} according to the previous rows"
209
+ end
210
+ }
211
+ each_cell_with_index do |cell, i, n_row|
212
+ if actual_row != n_row # is new row
213
+ check_last_row.call
214
+ actual_row = n_row
215
+ end
216
+ old_index = i + cell.colspan - 1
217
+ end
218
+ check_last_row.call
219
+ end
220
+
221
+ # An iterator method around renderable_data method.
222
+ #
223
+ # The issue using renderable_data is that in each iteration you don't know
224
+ # the real index for that cell, due to colspan & rowspan values of the
225
+ # previous cells.
226
+ #
227
+ # So this method yields every cell (Prawn::FlexibleTable::Cell) with its column
228
+ # index.
229
+ #
230
+ # Example:
231
+ # +-----------+
232
+ # | A | B |
233
+ # +-------+---+
234
+ # | C | D | E |
235
+ # +---+---+---+
236
+ # The values in each iteration will be:
237
+ # * Cell A, 0, 0
238
+ # * Cell B, 2, 0
239
+ # * Cell C, 0, 1
240
+ # * Cell D, 1, 1
241
+ # * Cell E, 2, 1
242
+ #
243
+ def each_cell_with_index
244
+ rowspan_cells = {}
245
+ n_row = 0
246
+ renderable_data.each do |row|
247
+ index = 0
248
+ rowspan_cells.each_value { |v| v[:rowspan] -= 1 }
249
+ rowspan_cells.delete_if { |k, v| v[:rowspan] == 0 }
250
+ row.each do |cell|
251
+ while rowspan_cells[ index ] do
252
+ index += rowspan_cells[ index ][:colspan]
253
+ end
254
+
255
+ yield cell, index, n_row
256
+
257
+ if cell.rowspan > 1
258
+ rowspan_cells[ index ] = { :rowspan => cell.rowspan,
259
+ :colspan => cell.colspan }
260
+ end
261
+ index += cell.colspan
262
+ end # row.each
263
+ n_row += 1
264
+ end # renderable_data.each
265
+ end
266
+
267
+ def cells_width( cell )
268
+ width = 2 * C(:horizontal_padding) + cell.to_s.lines.map do |e|
269
+ @document.width_of(e, :size => C(:font_size))
270
+ end.max.to_f
271
+ width.ceil
272
+ end
273
+
274
+ def calculate_column_widths(manual_widths=nil, width=nil)
275
+ @column_widths = [0] * @data[0].inject(0){ |total, e| total + e.colspan }
276
+
277
+ # Firstly, calculate column widths for cells without colspan attribute
278
+ colspan_cell_to_proccess = []
279
+ each_cell_with_index do |cell, index|
280
+ if cell.colspan <= 1
281
+ length = cells_width( cell )
282
+ @column_widths[ index ] = length if length > @column_widths[ index ]
283
+ else
284
+ colspan_cell_to_proccess << [ cell, index ]
285
+ end
286
+ end
287
+
288
+ # Secondly, calculate column width for cells with colspan attribute
289
+ # and update @column_widths properly
290
+ colspan_cell_to_proccess.each do |cell, index|
291
+ current_colspan = cell.colspan
292
+ calculate_width = @column_widths.slice( index, current_colspan ).
293
+ inject( 0 ) { |t, w| t + w }
294
+ length = cells_width( cell )
295
+ if length > calculate_width
296
+ # This is a little tricky, we have to increase each column
297
+ # that the actual colspan cell use, by a proportional part
298
+ # so the sum of these widths will be equal to the actual width
299
+ # of our colspan cell
300
+ difference = length - calculate_width
301
+ increase = ( difference / current_colspan ).floor
302
+ increase_by = [ increase ] * current_colspan
303
+ # it's important to sum, in total, the difference, so if
304
+ # difference is, e.g., 3 and current_colspan is 2, increase_by
305
+ # will be [ 1, 1 ], but actually we want to be [ 2, 1 ]
306
+ extra_dif = difference - increase * current_colspan
307
+ extra_dif.times { |n| increase_by[n] += 1 }
308
+ current_colspan.times do |j|
309
+ @column_widths[ index + j ] += increase_by[j]
310
+ end
311
+ end
312
+ end
313
+
314
+ # Thridly, establish manual column widths
315
+ manual_width = 0
316
+ manual_widths.each { |k,v|
317
+ @column_widths[k] = v; manual_width += v } if manual_widths
318
+
319
+ # Finally, ensures that the maximum width of the document is not exceeded.
320
+ # Takes into consideration the manual widths specified (With full manual
321
+ # widths specified, the width can exceed the document width as manual
322
+ # widths are taken as gospel)
323
+ max_width = width || @document.margin_box.width
324
+ calculated_width = @column_widths.inject {|sum,e| sum += e }
325
+
326
+ if calculated_width > max_width
327
+ shrink_by = (max_width - manual_width).to_f /
328
+ (calculated_width - manual_width)
329
+ @column_widths.each_with_index { |c,i|
330
+ @column_widths[i] = c * shrink_by if manual_widths.nil? ||
331
+ manual_widths[i].nil?
332
+ }
333
+ elsif width && calculated_width < width
334
+ grow_by = (width - manual_width).to_f /
335
+ (calculated_width - manual_width)
336
+ @column_widths.each_with_index { |c,i|
337
+ @column_widths[i] = c * grow_by if manual_widths.nil? ||
338
+ manual_widths[i].nil?
339
+ }
340
+ end
341
+ end
342
+
343
+
344
+ def renderable_data
345
+ C(:headers) ? C(:headers) + @data : @data
346
+ end
347
+
348
+ # Transform all items from @data into Prawn::FlexibleTable::Cell objects
349
+ def normalize_data
350
+ normalize = lambda { |data|
351
+ data.map do |row|
352
+ row.map do |cell|
353
+ unless cell.is_a?( Hash ) || cell.is_a?( Prawn::FlexibleTable::Cell )
354
+ cell = { :text => cell.to_s }
355
+ end
356
+ if cell.is_a?( Hash )
357
+ cell = Prawn::FlexibleTable::Cell.new( cell )
358
+ end
359
+ cell.document = @document
360
+ cell
361
+ end
362
+ end
363
+ }
364
+ @data = normalize.call( @data )
365
+ # C is an alias to configuration method, which is a wrapper around @config
366
+ @config[:headers] = normalize.call( [ C(:headers) ] ) if C(:headers)
367
+
368
+ end
369
+
370
+ def generate_table
371
+ page_contents = []
372
+ y_pos = @document.y
373
+ rowspan_cells = {}
374
+
375
+ @document.font_size C(:font_size) do
376
+ renderable_data.each_with_index do |row, index|
377
+ c = Prawn::FlexibleTable::CellBlock.new(@document)
378
+
379
+ rowspan_cells.each_value { |v| v[:rowspan] -= 1 }
380
+ rowspan_cells.delete_if { |k, v| v[:rowspan] == 0 }
381
+
382
+ col_index = 0
383
+ row.each do |e|
384
+ align = case C(:align)
385
+ when Hash
386
+ C(:align)[ col_index ]
387
+ else
388
+ C(:align)
389
+ end
390
+ align ||= e.to_s =~ NUMBER_PATTERN ? :right : :left
391
+
392
+ while rowspan_cells[ col_index ] do
393
+ c << rowspan_cells[ col_index ][:cell_fake]
394
+ col_index += rowspan_cells[ col_index ][:colspan]
395
+ end
396
+
397
+ colspan = e.colspan
398
+ rowspan = e.rowspan
399
+
400
+ width = @column_widths.
401
+ slice( col_index, colspan ).
402
+ inject { |sum, width| sum + width }
403
+
404
+ e.width = width
405
+ e.horizontal_padding = C(:horizontal_padding)
406
+ e.vertical_padding = C(:vertical_padding)
407
+ e.border_width = C(:border_width)
408
+ e.align ||= align
409
+
410
+ if rowspan > 1
411
+ cell_fake = Prawn::FlexibleTable::CellFake.new( :width => width )
412
+ rowspan_cells[ col_index ] = {
413
+ :rowspan => rowspan,
414
+ :colspan => colspan,
415
+ :cell_fake => cell_fake
416
+ }
417
+ end
418
+ c << e
419
+ col_index += colspan
420
+ end # row.each do |e|
421
+
422
+ bbox = @parent_bounds.stretchy? ? @document.margin_box : @parent_bounds
423
+ fit_in_current_page = c.height <= y_pos - bbox.absolute_bottom
424
+ if ! fit_in_current_page then
425
+ if C(:headers) && page_contents.length == 1
426
+ @document.start_new_page
427
+ y_pos = @document.y
428
+ else
429
+ draw_page(page_contents)
430
+ @document.start_new_page
431
+ if C(:headers) && page_contents.any?
432
+ page_contents = [page_contents[0]]
433
+ y_pos = @document.y - page_contents[0].height
434
+ else
435
+ page_contents = []
436
+ y_pos = @document.y
437
+ end
438
+ end
439
+ end
440
+
441
+ page_contents << c
442
+
443
+ y_pos -= c.height
444
+
445
+ if index == renderable_data.length - 1
446
+ draw_page(page_contents)
447
+ end
448
+
449
+ end
450
+ end
451
+ end
452
+
453
+ def draw_page(contents)
454
+ return if contents.empty?
455
+
456
+ if C(:border_style) == :underline_header
457
+ contents.each { |e| e.border_style = :none }
458
+ contents.first.border_style = :bottom_only if C(:headers)
459
+ elsif C(:border_style) == :grid || contents.length == 1
460
+ contents.each { |e| e.border_style = :all }
461
+ else
462
+ contents.first.border_style = C(:headers) ? :all : :no_bottom
463
+ contents.last.border_style = :no_top
464
+ end
465
+
466
+ if C(:headers)
467
+ contents.first.cells.each_with_index do |e,i|
468
+ if C(:align_headers)
469
+ case C(:align_headers)
470
+ when Hash
471
+ align = C(:align_headers)[i]
472
+ else
473
+ align = C(:align_headers)
474
+ end
475
+ end
476
+ e.align = align if align
477
+ e.text_color = C(:header_text_color) if C(:header_text_color)
478
+ e.background_color = C(:header_color) if C(:header_color)
479
+ end
480
+ end
481
+
482
+ # modified the height of the cells with rowspan attribute
483
+ contents.each_with_index do |x, i|
484
+ x.cells.each do |cell|
485
+ if cell.rowspan > 1
486
+ heights_per_row ||= contents.map { |x| x.height }
487
+ cell.height = heights_per_row.
488
+ slice( i, cell.rowspan ).inject(0){ |sum, h| sum + h }
489
+ end
490
+ end
491
+ end
492
+
493
+ contents.each do |x|
494
+ unless x.background_color
495
+ x.background_color = next_row_color if C(:row_colors)
496
+ end
497
+ x.border_color = C(:border_color) if C(:border_color)
498
+
499
+ x.draw
500
+ end
501
+
502
+ reset_row_colors
503
+ end
504
+
505
+
506
+ def next_row_color
507
+ color = C(:row_colors).shift
508
+ C(:row_colors).push(color)
509
+ color
510
+ end
511
+
512
+ def reset_row_colors
513
+ C(:row_colors => C(:original_row_colors).dup) if C(:row_colors)
514
+ end
515
+
516
+ end