pdf-wrapper 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ v0.3.0 (15th October 2009)
2
+ - remove some deprecated functions from Table
3
+ - added support for table cells with text and images
4
+ - no API incompatible changes to 0.2.1
5
+
1
6
  v0.2.1 (18th November 2008)
2
7
  - Small bugfix to prevent unnecesary STDERR output by pango
3
8
 
data/Rakefile CHANGED
@@ -5,8 +5,10 @@ require 'rake/rdoctask'
5
5
  require 'rake/testtask'
6
6
  require "rake/gempackagetask"
7
7
  require 'spec/rake/spectask'
8
+ require 'roodi'
9
+ require 'roodi_task'
8
10
 
9
- PKG_VERSION = "0.2.1"
11
+ PKG_VERSION = "0.3.0"
10
12
  PKG_NAME = "pdf-wrapper"
11
13
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
12
14
 
@@ -84,3 +86,5 @@ Rake::GemPackageTask.new(spec) do |pkg|
84
86
  pkg.need_zip = true
85
87
  pkg.need_tar = true
86
88
  end
89
+
90
+ RoodiTask.new 'roodi', ['lib/**/*.rb']
data/examples/table.rb CHANGED
@@ -31,6 +31,7 @@ table = PDF::Wrapper::Table.new(:font_size => 10) do |t|
31
31
  t.col_options 3, {:alignment => :centre, :border => "tb"}
32
32
  t.col_options :even, {:fill_color => :blue}
33
33
  t.cell_options 3, 3, {:fill_color => :green}
34
+ #t.manual_col_width 0, 200
34
35
  end
35
36
 
36
37
  pdf.table(table)
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
5
+
6
+ require 'pdf/wrapper'
7
+
8
+ pdf = PDF::Wrapper.new("image_table.pdf", :paper => :A4)
9
+ pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8.txt").strip, :alignment => :centre
10
+ pdf.pad 5
11
+ headers = %w{one two three four}
12
+
13
+ image_cell = PDF::Wrapper::TextImageCell.new("9781857233001", File.dirname(__FILE__) + "/../specs/data/orc.svg", 150, 100)
14
+
15
+ data = []
16
+ data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
17
+ data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
18
+ data << [image_cell,2,3,4]
19
+
20
+ data << [[], "j", "a", "m"]
21
+
22
+ (1..100).each do
23
+ data << %w{1 2 3 4}
24
+ end
25
+
26
+ table = PDF::Wrapper::Table.new(:font_size => 10) do |t|
27
+ t.data = data
28
+ t.headers headers, {:color => :white, :fill_color => :black}
29
+ t.row_options 6, {:border => "t"}
30
+ t.row_options :even, {:fill_color => :gray}
31
+ t.col_options 0, {:border => "tb"}
32
+ t.col_options 1, {:alignment => :centre}
33
+ t.col_options 2, {:alignment => :centre}
34
+ t.col_options 3, {:alignment => :centre, :border => "tb"}
35
+ t.col_options :even, {:fill_color => :blue}
36
+ t.cell_options 3, 3, {:fill_color => :green}
37
+ #t.manual_col_width 0, 200
38
+ end
39
+
40
+ pdf.table(table)
41
+ pdf.finish
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
5
+
6
+ require 'pdf/wrapper'
7
+
8
+ pdf = PDF::Wrapper.new("table.pdf", :paper => :A4)
9
+
10
+ data = []
11
+ data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
12
+ data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
13
+ data << [[], "j", "a", "m"]
14
+
15
+ (1..100).each do
16
+ data << %w{1 2 3 4}
17
+ end
18
+
19
+ pdf.table(data)
20
+ pdf.finish
data/lib/pdf/wrapper.rb CHANGED
@@ -9,6 +9,8 @@ require 'fileutils'
9
9
  require File.dirname(__FILE__) + "/wrapper/graphics"
10
10
  require File.dirname(__FILE__) + "/wrapper/images"
11
11
  require File.dirname(__FILE__) + "/wrapper/loading"
12
+ require File.dirname(__FILE__) + "/wrapper/text_cell"
13
+ require File.dirname(__FILE__) + "/wrapper/text_image_cell"
12
14
  require File.dirname(__FILE__) + "/wrapper/table"
13
15
  require File.dirname(__FILE__) + "/wrapper/text"
14
16
  require File.dirname(__FILE__) + "/wrapper/page"
@@ -1,8 +1,10 @@
1
1
  module PDF
2
2
  class Wrapper
3
3
 
4
- # Draws a basic table of text on the page. See the documentation for a detailed description of
5
- # how to control the table and its appearance.
4
+ # Draws a basic table of text on the page. See the documentation for PDF::Wrapper::Table to get
5
+ # a detailed description of how to control the table and its appearance. If data is an array,
6
+ # it can contain Cell-like objects (see PDF::Wrapper::TextCell and PDF::Wrapper::TextImageCell)
7
+ # or any objects that respond to to_s().
6
8
  #
7
9
  # <tt>data</tt>:: a 2d array with the data for the columns, or a PDF::Wrapper::Table object
8
10
  #
@@ -17,7 +19,7 @@ module PDF
17
19
  # <tt>:width</tt>:: The width of the table. Defaults to the distance from the left of the table to the right margin
18
20
  def table(data, opts = {})
19
21
  # TODO: add support for a table footer
20
- # - repeating each page, or just at the bottom?
22
+ # - repeating each page, or just at the bottom?
21
23
  # - if it repeats, should it be different on each page? ie. a sum of that pages rows, etc.
22
24
  # TODO: maybe support for multiple data sets with group headers/footers. useful for subtotals, etc
23
25
 
@@ -35,137 +37,13 @@ module PDF
35
37
  end
36
38
 
37
39
  t.width = options[:width] || points_to_right_margin(options[:left])
38
- calc_table_dimensions t
39
-
40
- # move to the start of our table (the top left)
41
- move_to(options[:left], options[:top])
42
-
43
- # draw the header cells
44
- draw_table_headers(t) if t.headers && (t.show_headers == :page || t.show_headers == :once)
45
-
46
- x, y = current_point
47
-
48
- # loop over each row in the table
49
- t.cells.each_with_index do |row, row_idx|
50
-
51
- # calc the height of the current row
52
- h = t.row_height(row_idx)
53
-
54
- if y + h > absolute_bottom_margin
55
- start_new_page
56
- y = margin_top
57
-
58
- # draw the header cells
59
- draw_table_headers(t) if t.headers && (t.show_headers == :page)
60
- x, y = current_point
61
- end
62
-
63
- # loop over each column in the current row
64
- row.each_with_index do |cell, col_idx|
65
-
66
- # calc the options and widths for this particular cell
67
- opts = t.options_for(col_idx, row_idx)
68
- w = t.col_width(col_idx)
69
-
70
- # paint it
71
- self.cell(cell.data, x, y, w, h, opts)
72
- x += w
73
- move_to(x, y)
74
- end
75
-
76
- # move to the start of the next row
77
- y += h
78
- x = options[:left]
79
- move_to(x, y)
80
- end
81
- end
82
-
83
- def calc_table_dimensions(t)
84
- # TODO: when calculating the min cell width, we basically want the width of the widest character. At the
85
- # moment I'm stripping all pango markup tags from the string, which means if any character is made
86
- # intentioanlly large, we'll miss it and it might not fit into our table cell.
87
-
88
- # calculate the min and max width of every cell in the table
89
- t.cells.each_with_index do |row, row_idx|
90
- row.each_with_index do |cell, col_idx|
91
- opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
92
- padding = opts[:padding] || 3
93
- if opts[:markup] == :pango
94
- str = cell.data.to_s.dup.gsub(/<.+?>/,"").gsub("&amp;","&").gsub("&lt;","<").gsub("&gt;",">")
95
- opts.delete(:markup)
96
- else
97
- str = cell.data.to_s.dup
98
- end
99
- cell.min_width = text_width(str.gsub(/\b|\B/,"\n"), opts) + (padding * 4)
100
- cell.max_width = text_width(str, opts) + (padding * 4)
101
- end
102
- end
103
-
104
- # calculate the min and max width of every cell in the headers row
105
- if t.headers
106
- t.headers.each_with_index do |cell, col_idx|
107
- opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
108
- padding = opts[:padding] || 3
109
- if opts[:markup] == :pango
110
- str = cell.data.to_s.dup.gsub(/<.+?>/,"").gsub("&amp;","&").gsub("&lt;","<").gsub("&gt;",">")
111
- opts.delete(:markup)
112
- else
113
- str = cell.data.to_s.dup
114
- end
115
- cell.min_width = text_width(str.gsub(/\b|\B/,"\n"), opts) + (padding * 4)
116
- cell.max_width = text_width(str, opts) + (padding * 4)
117
- end
118
- end
119
-
120
- # let the table decide on the actual widths it will use for each col
121
- t.calc_col_widths!
122
-
123
- # now that we know how wide each column will be, we can calculate the
124
- # height of every cell in the table
125
- t.cells.each_with_index do |row, row_idx|
126
- row.each_with_index do |cell, col_idx|
127
- opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
128
- padding = opts[:padding] || 3
129
- cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
130
- end
131
- end
132
-
133
- # let the table calculate how high each row is going to be
134
- t.calc_row_heights!
135
-
136
- # perform the same height calcs for the header row if necesary
137
- if t.headers
138
- t.headers.each_with_index do |cell, col_idx|
139
- opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
140
- padding = opts[:padding] || 3
141
- cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
142
- end
143
- t.calc_headers_height!
144
- end
145
- end
146
- private :calc_table_dimensions
147
-
148
- def draw_table_headers(t)
149
- x, y = current_point
150
- origx = x
151
- h = t.headers_height
152
- t.headers.each_with_index do |cell, col_idx|
153
- # calc the options and widths for this particular header cell
154
- opts = t.options_for(col_idx, :headers)
155
- w = t.col_width(col_idx)
156
-
157
- # paint it
158
- self.cell(cell.data, x, y, w, h, opts)
159
- x += w
160
- move_to(x, y)
161
- end
162
- move_to(origx, y + h)
40
+ t.draw(self, options[:left], options[:top])
163
41
  end
164
- private :draw_table_headers
165
42
 
166
43
  # This class is used to hold all the data and options for a table that will
167
44
  # be added to a PDF::Wrapper document. Tables are a collection of cells, each
168
- # one rendered to the document using the Wrapper#cell function.
45
+ # one individually rendered to the document in a location that makes it appear
46
+ # to be a table.
169
47
  #
170
48
  # To begin working with a table, pass in a 2d array of data to display, along
171
49
  # with optional headings, then pass the object to Wrapper#table
@@ -178,13 +56,13 @@ module PDF
178
56
  # end
179
57
  # pdf.table(table)
180
58
  #
181
- # For all but the most basic tables, you will probably want to tweak at least
59
+ # For all but the most basic tables, you will probably want to tweak at least
182
60
  # some of the options for some of the cells. The options available are the same
183
61
  # as those that are valid for the Wrapper#cell method, including things like font,
184
62
  # font size, color and alignment.
185
63
  #
186
64
  # Options can be specified at the table, column, row and cell level. When it comes time
187
- # to render each cell, the options are merged together so that cell options override row
65
+ # to render each cell, the options are merged together so that cell options override row
188
66
  # ones, row ones override column ones and column ones override table wide ones.
189
67
  #
190
68
  # By default, no options are defined at all, and the document defaults will be used.
@@ -211,39 +89,73 @@ module PDF
211
89
  # to change this behaviour. Valid values are nil for never, :once for just the at the
212
90
  # top of the table, and :page for the default.
213
91
  #
92
+ # == Complex Cells
93
+ #
94
+ # By default, any cell content described in the data array is converted to a string and
95
+ # wrapped in a TextCell object. If you need to, it is possible to define your cells
96
+ # as cell-like objects manually to get more control.
97
+ #
98
+ # The following two calls are equivilant:
99
+ #
100
+ # data = [[1,2]]
101
+ # pdf.table(data)
102
+ #
103
+ # data = [[PDF::Wrapper::TextCell.new(2),PDF::Wrapper::TextCell.new(2)]]
104
+ # pdf.table(data)
105
+ #
106
+ # An alternative to a text-only cell is a cell with text and an image. These
107
+ # cells must be initialised with a filename and cell dimensions (width and height)
108
+ # as calculating automatic dimensions is difficult.
109
+ #
110
+ # data = [
111
+ # ["James", PDF::Wrapper::TextImageCell.new("Healy","photo-jim.jpg",100,100)],
112
+ # ["Jess", PDF::Wrapper::TextImageCell.new("Healy","photo-jess.jpg",100,100)],
113
+ # ]
114
+ # pdf.table(data)
115
+ #
116
+ # If TextImageCell doesn't meet your needs, you are free to define your own
117
+ # cell-like object and use that.
118
+ #
214
119
  class Table
215
- attr_reader :cells#, :headers
120
+ attr_reader :cells, :wrapper
216
121
  attr_accessor :width, :show_headers
217
122
 
218
- #
219
123
  def initialize(opts = {})
220
124
 
221
125
  # default table options
222
126
  @table_options = opts
223
- @col_options = Hash.new({})
224
- @row_options = Hash.new({})
225
127
  @manual_col_widths = {}
226
- @header_options = {}
227
128
  @show_headers = :page
228
129
 
229
130
  yield self if block_given?
230
131
  self
231
132
  end
232
133
 
233
- # Specify the tables data.
134
+ # Set the table data.
234
135
  #
235
136
  # The single argument should be a 2d array like:
236
137
  #
237
138
  # [[ "one", "two"],
238
139
  # [ "one", "two"]]
140
+ #
141
+ # The cells in the array can be any object with to_s() defined, or a Cell-like
142
+ # object (such as a TextCell or TextImageCell).
143
+ #
239
144
  def data=(d)
240
- # TODO: raise an exception of the data rows aren't all the same size
241
- # TODO: ensure d is array-like
145
+ row_sizes = d.map { |row| row.size }.compact.uniq
146
+ raise ArgumentError, "" if row_sizes.size > 1
147
+
242
148
  @cells = d.collect do |row|
243
- row.collect do |str|
244
- Wrapper::Cell.new(str)
149
+ row.collect do |data|
150
+ if data.kind_of?(Wrapper::TextCell) || data.kind_of?(Wrapper::TextImageCell)
151
+ data
152
+ else
153
+ Wrapper::TextCell.new(data.to_s)
154
+ end
245
155
  end
246
156
  end
157
+ each_cell { |cell| cell.options.merge!(@table_options)}
158
+ @cells
247
159
  end
248
160
 
249
161
  # Retrieve or set the table's optional column headers.
@@ -258,57 +170,78 @@ module PDF
258
170
  # t.headers ["col one", "col two]
259
171
  #
260
172
  # The optional second argument sets the cell options for the header
261
- # cells. See PDF::Wrapper#cell for a list of possible options.
173
+ # cells. See PDF::Wrapper#cell for a list of possible options.
262
174
  #
263
175
  # t.headers ["col one", "col two], :color => :block, :fill_color => :black
264
176
  #
265
- # If the options hash is left unspecified, the default table options will
177
+ # If the options hash is left unspecified, the default table options will
266
178
  # be used.
267
179
  #
268
180
  def headers(h = nil, opts = {})
269
- # TODO: raise an exception of the size of the array does not match the size
270
- # of the data row arrays
271
- # TODO: ensure h is array-like
272
181
  return @headers if h.nil?
273
- @headers = h.collect do |str|
274
- Wrapper::Cell.new(str)
182
+
183
+ if @cells && @cells.first.size != h.size
184
+ raise ArgumentError, "header column count does not match data column count"
275
185
  end
276
- @header_options = opts
277
- end
278
186
 
279
- def headers=(h)
280
- # TODO: remove this method at some point. Deprecation started on 10th August 2008.
281
- warn "WARNING: Table#headers=() is deprecated, headers should now be set along with header options using Table#headers()"
282
- headers h
187
+ @headers = h.collect do |data|
188
+ if data.kind_of?(Wrapper::TextCell) || data.kind_of?(Wrapper::TextImageCell)
189
+ data
190
+ else
191
+ Wrapper::TextCell.new(data.to_s)
192
+ end
193
+ end
194
+ @headers.each { |cell| cell.options.merge!(@table_options)}
195
+ @headers.each { |cell| cell.options.merge!(opts)}
196
+ @headers
283
197
  end
284
198
 
285
- # access a particular cell at location x, y
286
- def cell(col_idx, row_idx)
287
- @cells[row_idx, col_idx]
288
- end
199
+ def draw(wrapper, tablex, tabley)
200
+ @wrapper = wrapper
289
201
 
290
- # Set or retrieve options that apply to every cell in the table.
291
- # For a list of valid options, see Wrapper#cell.
292
- #
293
- # WARNING. This method is deprecated. Table options should be passed to the
294
- # PDF::Wrapper::Table constructor instead
295
- def table_options(opts = nil)
296
- # TODO: remove this method at some point. Deprecation started on 10th August 2008.
297
- warn "WARNING: Table#table_options() is deprecated, please see the documentation for PDF::Wrapper::Table"
298
- @table_options = @table_options.merge(opts) if opts
299
- @table_options
202
+ calculate_dimensions
203
+
204
+ # move to the start of our table (the top left)
205
+ wrapper.move_to(tablex, tabley)
206
+
207
+ # draw the header cells
208
+ draw_table_headers if self.headers && (self.show_headers == :page || self.show_headers == :once)
209
+
210
+ x, y = wrapper.current_point
211
+
212
+ # loop over each row in the table
213
+ self.cells.each_with_index do |row, row_idx|
214
+
215
+ # calc the height of the current row
216
+ h = row.first.height
217
+
218
+ if y + h > wrapper.absolute_bottom_margin
219
+ wrapper.start_new_page
220
+ y = wrapper.margin_top
221
+
222
+ # draw the header cells
223
+ draw_table_headers if self.headers && (self.show_headers == :page)
224
+ x, y = wrapper.current_point
225
+ end
226
+
227
+ # loop over each column in the current row and paint it
228
+ row.each_with_index do |cell, col_idx|
229
+ cell.draw(wrapper, x, y)
230
+ x += cell.width
231
+ wrapper.move_to(x, y)
232
+ end
233
+
234
+ # move to the start of the next row
235
+ y += h
236
+ x = tablex
237
+ wrapper.move_to(x, y)
238
+ end
300
239
  end
301
240
 
302
- # set or retrieve options that apply to header cells
303
- # For a list of valid options, see Wrapper#cell.
241
+ # access a particular cell at location x, y
304
242
  #
305
- # WARNING. This method is deprecated. Header options should be passed to the
306
- # PDF::Wrapper::Table#headers method instead
307
- def header_options(opts = nil)
308
- # TODO: remove this method at some point. Deprecation started on 10th August 2008.
309
- warn "WARNING: Table#header_options() is deprecated, please see the documentation for PDF::Wrapper::Table"
310
- @header_options = @header_options.merge(opts) if opts
311
- @header_options
243
+ def cell(col_idx, row_idx)
244
+ @cells[row_idx][col_idx]
312
245
  end
313
246
 
314
247
  # set or retrieve options that apply to a single cell
@@ -329,14 +262,16 @@ module PDF
329
262
  (spec.class == Range && spec.include?(col_idx)) ||
330
263
  (spec.class == Array && spec.include?(col_idx)) ||
331
264
  (spec.respond_to?(:to_i) && spec.to_i == col_idx)
332
-
333
- @col_options[col_idx] = @col_options[col_idx].merge(opts)
265
+
266
+ cells_in_col(col_idx).each do |cell|
267
+ cell.options.merge!(opts)
268
+ end
334
269
  end
335
270
  end
336
271
  self
337
272
  end
338
273
 
339
- # Manually set the width for 1 or more columns
274
+ # Manually set the width for 1 or more columns
340
275
  #
341
276
  # <tt>spec</tt>:: Which columns to set the width for. :odd, :even, a range, an Array of numbers or a number
342
277
  #
@@ -348,7 +283,7 @@ module PDF
348
283
  (spec.class == Range && spec.include?(col_idx)) ||
349
284
  (spec.class == Array && spec.include?(col_idx)) ||
350
285
  (spec.respond_to?(:to_i) && spec.to_i == col_idx)
351
-
286
+
352
287
  @manual_col_widths[col_idx] = width
353
288
  end
354
289
  end
@@ -365,174 +300,172 @@ module PDF
365
300
  (spec.class == Range && spec.include?(row_idx)) ||
366
301
  (spec.class == Array && spec.include?(row_idx)) ||
367
302
  (spec.respond_to?(:to_i) && spec.to_i == row_idx)
368
-
369
- @row_options[row_idx] = @col_options[row_idx].merge(opts)
303
+
304
+ cells_in_row(row_idx).each do |cell|
305
+ cell.options.merge!(opts)
306
+ end
370
307
  end
371
308
  end
372
309
  self
373
310
  end
374
311
 
375
- # calculate the combined options for a particular cell
376
- #
377
- # To get the options for a regular cell, use numbers to reference the exact cell:
378
- #
379
- # options_for(3, 3)
380
- #
381
- # To get options for a header cell, use :headers for the row:
382
- #
383
- # options_for(3, :headers)
312
+ # Returns the number of columns in the table
313
+ def col_count
314
+ @cells.first.size.to_f
315
+ end
316
+
317
+ # iterate over each cell in the table. Yields a cell object.
384
318
  #
385
- def options_for(col_idx, row_idx = nil)
386
- opts = @table_options.dup
387
- opts.merge! @col_options[col_idx]
388
- if row_idx == :headers
389
- opts.merge! @header_options
390
- else
391
- opts.merge! @row_options[row_idx]
392
- opts.merge! @cells[row_idx][col_idx].options
319
+ def each_cell(&block)
320
+ each_row do |row_idx|
321
+ cells_in_row(row_idx).each do |cell|
322
+ yield cell
323
+ end
393
324
  end
394
- opts
395
325
  end
396
326
 
397
- # Returns the required height for the headers row.
398
- # Essentially just the height of the tallest cell in the row.
399
- def headers_height
400
- raise "You must call calc_headers_height! before calling headers_height" if @headers_height.nil?
401
- @headers_height
402
- end
327
+ private
403
328
 
404
- # Returns the required height for a particular row.
405
- # Essentially just the height of the tallest cell in the row.
406
- def row_height(idx)
407
- raise "You must call calc_row_heights! before calling row_heights" if @row_heights.nil?
408
- @row_heights[idx]
329
+ def draw_table_headers
330
+ x, y = wrapper.current_point
331
+ origx = x
332
+ h = self.headers.first.height
333
+ self.headers.each_with_index do |cell, col_idx|
334
+ cell.draw(wrapper, x, y)
335
+ x += cell.width
336
+ wrapper.move_to(x, y)
337
+ end
338
+ wrapper.move_to(origx, y + h)
409
339
  end
410
340
 
411
- # Returns the number of columns in the table
412
- def col_count
413
- @cells.first.size.to_f
341
+ # calculate the dimensions of each row and column in the table. The order
342
+ # here is crucial. First we ask each cell to caclulate the range of
343
+ # widths they can render with, then we make a decision on the actual column
344
+ # width and pass that on to every cell.
345
+ #
346
+ # Once each cell knows how wide it will be it can calculate how high it
347
+ # will be. With that done the table cen determine the tallest cell in
348
+ # each row and pass that onto each cell so every cell in a row renders
349
+ # with the same height.
350
+ #
351
+ def calculate_dimensions
352
+ calculate_cell_width_range
353
+ calculate_column_widths
354
+ calculate_cell_heights
355
+ calculate_row_heights
414
356
  end
415
357
 
416
- # Returns the width of the specified column
417
- def col_width(idx)
418
- raise "You must call calc_col_widths! before calling col_width" if @col_widths.nil?
419
- @col_widths[idx]
420
- end
358
+ def calculate_cell_width_range
359
+ # TODO: when calculating the min cell width, we basically want the width of the widest character. At the
360
+ # moment I'm stripping all pango markup tags from the string, which means if any character is made
361
+ # intentioanlly large, we'll miss it and it might not fit into our table cell.
421
362
 
422
- # process the individual cell widths and decide on the resulting
423
- # width of each column in the table
424
- def calc_col_widths!
425
- @col_widths = calc_column_widths
363
+ # calculate the min and max width of every cell in the table
364
+ cells.each_with_index do |row, row_idx|
365
+ row.each_with_index do |cell, col_idx|
366
+ cell.calculate_width_range(wrapper)
367
+ end
368
+ end
369
+
370
+ # calculate the min and max width of every cell in the headers row
371
+ if self.headers
372
+ self.headers.each_with_index do |cell, col_idx|
373
+ cell.calculate_width_range(wrapper)
374
+ end
375
+ end
426
376
  end
427
377
 
428
- # process the individual cell heights in the header and decide on the
429
- # resulting height of each row in the table
430
- def calc_headers_height!
431
- @headers_height = @headers.collect { |cell| cell.height }.compact.max
378
+ def calculate_cell_heights
379
+ cells.each_with_index do |row, row_idx|
380
+ row.each_with_index do |cell, col_idx|
381
+ cell.calculate_height(wrapper)
382
+ end
383
+ end
384
+
385
+ # perform the same height calcs for the header row if necesary
386
+ if self.headers
387
+ self.headers.each_with_index do |cell, col_idx|
388
+ cell.calculate_height(wrapper)
389
+ end
390
+ end
432
391
  end
433
392
 
434
393
  # process the individual cell heights and decide on the resulting
435
394
  # height of each row in the table
436
- def calc_row_heights!
437
- @row_heights = @cells.collect do |row|
438
- row.collect { |cell| cell.height }.compact.max
395
+ def calculate_row_heights
396
+ @cells.each do |row|
397
+ row_height = row.collect { |cell| cell.height }.compact.max
398
+ row.each { |cell| cell.height = row_height }
439
399
  end
440
- end
441
400
 
442
- # forget row and column dimensions
443
- def reset!
444
- @col_widths = nil
445
- @row_heights = nil
401
+ if @headers
402
+ row_height = @headers.collect { |cell| cell.height }.compact.max
403
+ self.headers.each_with_index do |cell, col_idx|
404
+ cell.height = row_height
405
+ end
406
+ end
446
407
  end
447
408
 
448
- private
449
-
450
409
  # the main smarts behind deciding on the width of each column. If possible,
451
410
  # each cell will get the maximum amount of space it wants. If not, some
452
411
  # negotiation happens to find the best possible set of widths.
453
- def calc_column_widths
412
+ #
413
+ def calculate_column_widths
454
414
  raise "Can't calculate column widths without knowing the overall table width" if self.width.nil?
455
- check_cell_widths
456
415
 
457
- max_col_widths = {}
458
416
  min_col_widths = {}
417
+ natural_col_widths = {}
418
+ max_col_widths = {}
459
419
  each_column do |col|
460
- min_col_widths[col] = cells_in_col(col).collect { |c| c.min_width}.max.to_f
461
- max_col_widths[col] = cells_in_col(col).collect { |c| c.max_width}.max.to_f
462
- end
463
- # add header cells to the mix
464
- if @headers
465
- @headers.each_with_index do |cell, idx|
466
- min_col_widths[idx] = [cell.min_width.to_f, min_col_widths[idx]].max
467
- max_col_widths[idx] = [cell.max_width.to_f, max_col_widths[idx]].max
468
- end
420
+ min_col_widths[col] = cells_in_col(col).collect { |c| c.min_width}.max
421
+ natural_col_widths[col] = cells_in_col(col).collect { |c| c.natural_width}.max
422
+ max_col_widths[col] = cells_in_col(col).collect { |c| c.max_width}.compact.max
469
423
  end
470
424
 
471
425
  # override the min and max col widths with manual ones where appropriate
472
- # freeze the values so that the algorithm that adjusts the widths
473
- # leaves them untouched
474
- @manual_col_widths.each { |key, val| val.freeze }
475
426
  max_col_widths.merge! @manual_col_widths
427
+ natural_col_widths.merge! @manual_col_widths
476
428
  min_col_widths.merge! @manual_col_widths
477
429
 
478
430
  if min_col_widths.values.sum > self.width
479
431
  raise RuntimeError, "table content cannot fit into a table width of #{self.width}"
480
- end
481
-
482
- if max_col_widths.values.sum == self.width
483
- # every col gets the space it wants
484
- col_widths = max_col_widths.dup
485
- elsif max_col_widths.values.sum < self.width
486
- # every col gets the space it wants, and there's
487
- # still more room left. Distribute the extra room evenly
488
- col_widths = grow_col_widths(max_col_widths.dup, max_col_widths, true)
489
432
  else
490
433
  # there's not enough room for every col to get as much space
491
434
  # as it wants, so work our way down until it fits
492
- col_widths = grow_col_widths(min_col_widths.dup, max_col_widths, false)
493
- end
494
- col_widths
495
- end
496
-
497
- # check to ensure every cell has a minimum and maximum cell width defined
498
- def check_cell_widths
499
- @cells.each do |row|
500
- row.each_with_index do |cell, col_idx|
501
- raise "Every cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
502
- raise "Every cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
503
- if @manual_col_widths[col_idx] && cell.min_width > @manual_col_widths[col_idx]
504
- raise "Manual width for col #{col_idx} is too low"
505
- end
506
- end
507
- end
508
- if @headers
509
- @headers.each_with_index do |cell, col_idx|
510
- raise "Every header cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
511
- raise "Every header cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
512
- if @manual_col_widths[col_idx] && cell.min_width > @manual_col_widths[col_idx]
513
- raise "Manual width for col #{col_idx} is too low"
435
+ col_widths = grow_col_widths(min_col_widths.dup, natural_col_widths, max_col_widths)
436
+ col_widths.each do |col_index, width|
437
+ cells_in_col(col_index).each do |cell|
438
+ cell.width = width
514
439
  end
515
440
  end
516
441
  end
517
442
  end
518
443
 
519
- # iterate over each column in the table
444
+ # iterate over each column in the table. Yields a column index, not
445
+ # actual columns or cells.
446
+ #
520
447
  def each_column(&block)
521
448
  (0..(col_count-1)).each do |col|
522
449
  yield col
523
450
  end
524
451
  end
525
452
 
526
- # iterate over each row in the table
453
+ # iterate over each row in the table. Yields an row index, not actual rows
454
+ # or cells.
455
+ #
527
456
  def each_row(&block)
528
457
  (0..(@cells.size-1)).each do |row|
529
458
  yield row
530
459
  end
531
460
  end
532
461
 
533
- # an array of all the cells in the specified column
462
+ # an array of all the cells in the specified column, including headers
463
+ #
534
464
  def cells_in_col(idx)
535
- @cells.collect {|row| row[idx]}
465
+ ret = []
466
+ ret << @headers[idx] if @headers
467
+ ret += @cells.collect {|row| row[idx]}
468
+ ret
536
469
  end
537
470
 
538
471
  # an array of all the cells in the specified row
@@ -540,17 +473,35 @@ module PDF
540
473
  @cells[idx]
541
474
  end
542
475
 
543
- # if the widths of every column are less than the total width
544
- # of the table, grow them to make use of it.
476
+ # starting with very low widths for each col, bump each column width up
477
+ # until we reach the width of the entire table.
478
+ #
479
+ # columns that are less than their "natural width" are given preference.
480
+ # If every column has reached its natural width then each column is
481
+ # increased in an equal manor.
545
482
  #
546
- # col_widths - the current hash of widths for each column index
547
- # max_col_widths - the maximum width each column desires
548
- # past_max - can the width of a colum grow beyond its maximum desired
549
- def grow_col_widths(col_widths, max_col_widths, past_max = false)
483
+ # starting col_widths
484
+ # the hash of column widths to start from. Should generally match the
485
+ # absolute smallest width each column can render in
486
+ # natural_col_widths
487
+ # the hqash of column widths where each column will be able to render
488
+ # itself fully without wrapping
489
+ # max_col_widths
490
+ # the hash of absolute maximum column widths, no column width can go
491
+ # past this. Can be nil, which indicates there's no maximum
492
+ #
493
+ def grow_col_widths(starting_col_widths, natural_col_widths, max_col_widths)
494
+ col_widths = starting_col_widths.dup
550
495
  loop do
551
496
  each_column do |idx|
552
- col_widths[idx] += 0.3 unless col_widths[idx].frozen?
553
- col_widths[idx].freeze if col_widths[idx] >= max_col_widths[idx] && past_max == false
497
+ if col_widths.values.sum >= natural_col_widths.values.sum ||
498
+ col_widths[idx] < natural_col_widths[idx]
499
+ if max_col_widths[idx].nil? || col_widths[idx] < max_col_widths[idx]
500
+ col_widths[idx] += 0.3
501
+ else
502
+ col_widths[idx] = max_col_widths[idx]
503
+ end
504
+ end
554
505
  break if col_widths.values.sum >= self.width
555
506
  end
556
507
  break if col_widths.values.sum >= self.width
@@ -558,15 +509,5 @@ module PDF
558
509
  col_widths
559
510
  end
560
511
  end
561
-
562
- # A basic container to hold the required information for each cell
563
- class Cell
564
- attr_accessor :data, :options, :height, :min_width, :max_width
565
-
566
- def initialize(str)
567
- @data = str
568
- @options = {}
569
- end
570
- end
571
512
  end
572
513
  end