pdf-wrapper 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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