prawn-table-continued 1.0.0.rc1

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +340 -0
  4. data/GPLv3 +674 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +56 -0
  7. data/lib/prawn/table/cell/image.rb +69 -0
  8. data/lib/prawn/table/cell/in_table.rb +33 -0
  9. data/lib/prawn/table/cell/span_dummy.rb +93 -0
  10. data/lib/prawn/table/cell/subtable.rb +66 -0
  11. data/lib/prawn/table/cell/text.rb +155 -0
  12. data/lib/prawn/table/cell.rb +787 -0
  13. data/lib/prawn/table/cells.rb +261 -0
  14. data/lib/prawn/table/column_width_calculator.rb +182 -0
  15. data/lib/prawn/table/version.rb +5 -0
  16. data/lib/prawn/table.rb +711 -0
  17. data/manual/contents.rb +13 -0
  18. data/manual/example_helper.rb +8 -0
  19. data/manual/images/prawn.png +0 -0
  20. data/manual/images/stef.jpg +0 -0
  21. data/manual/table/basic_block.rb +53 -0
  22. data/manual/table/before_rendering_page.rb +26 -0
  23. data/manual/table/cell_border_lines.rb +24 -0
  24. data/manual/table/cell_borders_and_bg.rb +31 -0
  25. data/manual/table/cell_dimensions.rb +36 -0
  26. data/manual/table/cell_text.rb +38 -0
  27. data/manual/table/column_widths.rb +30 -0
  28. data/manual/table/content_and_subtables.rb +39 -0
  29. data/manual/table/creation.rb +27 -0
  30. data/manual/table/filtering.rb +36 -0
  31. data/manual/table/flow_and_header.rb +17 -0
  32. data/manual/table/image_cells.rb +33 -0
  33. data/manual/table/position.rb +29 -0
  34. data/manual/table/row_colors.rb +20 -0
  35. data/manual/table/span.rb +30 -0
  36. data/manual/table/style.rb +33 -0
  37. data/manual/table/table.rb +52 -0
  38. data/manual/table/width.rb +27 -0
  39. data/prawn-table.gemspec +36 -0
  40. data/spec/cell_spec.rb +652 -0
  41. data/spec/extensions/encoding_helpers.rb +11 -0
  42. data/spec/extensions/file_fixture_helper.rb +15 -0
  43. data/spec/fixtures/files/prawn.png +0 -0
  44. data/spec/spec_helper.rb +50 -0
  45. data/spec/table/span_dummy_spec.rb +26 -0
  46. data/spec/table_spec.rb +1626 -0
  47. metadata +234 -0
@@ -0,0 +1,711 @@
1
+ # encoding: utf-8
2
+ #
3
+ # table.rb: Table drawing functionality.
4
+ #
5
+ # Copyright December 2009, Brad Ediger. All rights reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+
10
+ require 'prawn'
11
+ require_relative 'table/column_width_calculator'
12
+ require_relative 'table/cell'
13
+ require_relative 'table/cells'
14
+ require_relative 'table/cell/in_table'
15
+ require_relative 'table/cell/text'
16
+ require_relative 'table/cell/subtable'
17
+ require_relative 'table/cell/image'
18
+ require_relative 'table/cell/span_dummy'
19
+
20
+ module Prawn
21
+ module Errors
22
+ # This error is raised when table data is malformed
23
+ #
24
+ InvalidTableData = Class.new(StandardError)
25
+
26
+ # This error is raised when an empty or nil table is rendered
27
+ #
28
+ EmptyTable = Class.new(StandardError)
29
+
30
+ # Raised when unrecognized content is provided for a table cell.
31
+ #
32
+ UnrecognizedTableContent = Class.new(StandardError) unless defined?(::Prawn::Errors::UnrecognizedTableContent)
33
+ end
34
+
35
+ # Next-generation table drawing for Prawn.
36
+ #
37
+ # = Data
38
+ #
39
+ # Data, for a Prawn table, is a two-dimensional array of objects that can be
40
+ # converted to cells ("cellable" objects). Cellable objects can be:
41
+ #
42
+ # String::
43
+ # Produces a text cell. This is the most common usage.
44
+ # Prawn::Table::Cell::
45
+ # If you have already built a Cell or have a custom subclass of Cell you
46
+ # want to use in a table, you can pass through Cell objects.
47
+ # Prawn::Table::
48
+ # Creates a subtable (a table within a cell). You can use
49
+ # Prawn::Document#make_table to create a table for use as a subtable
50
+ # without immediately drawing it.
51
+ # Array::
52
+ # Creates a simple subtable. Create a Table object using make_table (see
53
+ # above) if you need more control over the subtable's styling.
54
+ #
55
+ # = Options
56
+ #
57
+ # Prawn/Layout provides many options to control style and layout of your
58
+ # table. These options are implemented with a uniform interface: the +:foo+
59
+ # option always sets the +foo=+ accessor. See the accessor and method
60
+ # documentation for full details on the options you can pass. Some
61
+ # highlights:
62
+ #
63
+ # +cell_style+::
64
+ # A hash of style options to style all cells. See the documentation on
65
+ # Prawn::Table::Cell for all cell style options.
66
+ # +header+::
67
+ # If set to +true+, the first row will be repeated on every page. If set
68
+ # to an Integer, the first +x+ rows will be repeated on every page. Row
69
+ # numbering (for styling and other row-specific options) always indexes
70
+ # based on your data array. Whether or not you have a header, row(n) always
71
+ # refers to the nth element (starting from 0) of the +data+ array.
72
+ # +column_widths+::
73
+ # Sets widths for individual columns. Manually setting widths can give
74
+ # better results than letting Prawn guess at them, as Prawn's algorithm
75
+ # for defaulting widths is currently pretty boneheaded. If you experience
76
+ # problems like weird column widths or CannotFit errors, try manually
77
+ # setting widths on more columns.
78
+ # +position+::
79
+ # Either :left (the default), :center, :right, or a number. Specifies the
80
+ # horizontal position of the table within its bounding box. If a number is
81
+ # provided, it specifies the distance in points from the left edge.
82
+ #
83
+ # = Initializer Block
84
+ #
85
+ # If a block is passed to methods that initialize a table
86
+ # (Prawn::Table.new, Prawn::Document#table, Prawn::Document#make_table), it
87
+ # will be called after cell setup but before layout. This is a very flexible
88
+ # way to specify styling and layout constraints. This code sets up a table
89
+ # where the second through the fourth rows (1-3, indexed from 0) are each one
90
+ # inch (72 pt) wide:
91
+ #
92
+ # pdf.table(data) do |table|
93
+ # table.rows(1..3).width = 72
94
+ # end
95
+ #
96
+ # As with Prawn::Document#initialize, if the block has no arguments, it will
97
+ # be evaluated in the context of the object itself. The above code could be
98
+ # rewritten as:
99
+ #
100
+ # pdf.table(data) do
101
+ # rows(1..3).width = 72
102
+ # end
103
+ #
104
+ class Table
105
+ module Interface
106
+ # @group Experimental API
107
+
108
+ # Set up and draw a table on this document. A block can be given, which will
109
+ # be run after cell setup but before layout and drawing.
110
+ #
111
+ # See the documentation on Prawn::Table for details on the arguments.
112
+ #
113
+ def table(data, options={}, &block)
114
+ t = Table.new(data, self, options, &block)
115
+ t.draw
116
+ t
117
+ end
118
+
119
+ # Set up, but do not draw, a table. Useful for creating subtables to be
120
+ # inserted into another Table. Call +draw+ on the resulting Table to ink it.
121
+ #
122
+ # See the documentation on Prawn::Table for details on the arguments.
123
+ #
124
+ def make_table(data, options={}, &block)
125
+ Table.new(data, self, options, &block)
126
+ end
127
+ end
128
+
129
+ # Set up a table on the given document. Arguments:
130
+ #
131
+ # +data+::
132
+ # A two-dimensional array of cell-like objects. See the "Data" section
133
+ # above for the types of objects that can be put in a table.
134
+ # +document+::
135
+ # The Prawn::Document instance on which to draw the table.
136
+ # +options+::
137
+ # A hash of attributes and values for the table. See the "Options" block
138
+ # above for details on available options.
139
+ #
140
+ def initialize(data, document, options={}, &block)
141
+ table_opts = options.dup
142
+ @pdf = document
143
+ @cells = make_cells(data, table_opts.delete(:cell_style) || {})
144
+ @header = false
145
+ table_opts.each do |k, v|
146
+ send("#{k}=", v) if respond_to?("#{k}=")
147
+ end
148
+
149
+ if block
150
+ block.arity < 1 ? instance_eval(&block) : block[self]
151
+ end
152
+
153
+ set_column_widths
154
+ set_row_heights
155
+ position_cells
156
+ end
157
+
158
+ # Number of rows in the table.
159
+ #
160
+ attr_reader :row_length
161
+
162
+ # Number of columns in the table.
163
+ #
164
+ attr_reader :column_length
165
+
166
+ # Manually set the width of the table.
167
+ #
168
+ attr_writer :width
169
+
170
+ # Position (:left, :right, :center, or a number indicating distance in
171
+ # points from the left edge) of the table within its parent bounds.
172
+ #
173
+ attr_writer :position
174
+
175
+ # Returns a Prawn::Table::Cells object representing all of the cells in
176
+ # this table.
177
+ #
178
+ attr_reader :cells
179
+
180
+ # Specify a callback to be called before each page of cells is rendered.
181
+ # The block is passed a Cells object containing all cells to be rendered on
182
+ # that page. You can change styling of the cells in this block, but keep in
183
+ # mind that the cells have already been positioned and sized.
184
+ #
185
+ def before_rendering_page(&block)
186
+ @before_rendering_page = block
187
+ end
188
+
189
+ # Returns the width of the table in PDF points.
190
+ #
191
+ def width
192
+ @width ||= [natural_width, @pdf.bounds.width].min
193
+ end
194
+
195
+ # Sets column widths for the table. The argument can be one of the following
196
+ # types:
197
+ #
198
+ # +Array+::
199
+ # <tt>[w0, w1, w2, ...]</tt> (specify a width for each column)
200
+ # +Hash+::
201
+ # <tt>{0 => w0, 1 => w1, ...}</tt> (keys are column names, values are
202
+ # widths)
203
+ # +Numeric+::
204
+ # +72+ (sets width for all columns)
205
+ #
206
+ def column_widths=(widths)
207
+ case widths
208
+ when Array
209
+ widths.each_with_index { |w, i| column(i).width = w }
210
+ when Hash
211
+ widths.each { |i, w| column(i).width = w }
212
+ when Numeric
213
+ cells.width = widths
214
+ else
215
+ raise ArgumentError, "cannot interpret column widths"
216
+ end
217
+ end
218
+
219
+ # Returns the height of the table in PDF points.
220
+ #
221
+ def height
222
+ cells.height
223
+ end
224
+
225
+ # If +true+, designates the first row as a header row to be repeated on
226
+ # every page. If an integer, designates the number of rows to be treated
227
+ # as a header Does not change row numbering -- row numbers always index
228
+ # into the data array provided, with no modification.
229
+ #
230
+ attr_writer :header
231
+
232
+ # Accepts an Array of alternating row colors to stripe the table.
233
+ #
234
+ attr_writer :row_colors
235
+
236
+ # Sets styles for all cells.
237
+ #
238
+ # pdf.table(data, :cell_style => { :borders => [:left, :right] })
239
+ #
240
+ def cell_style=(style_hash)
241
+ cells.style(style_hash)
242
+ end
243
+
244
+ # Allows generic stylable content. This is an alternate syntax that some
245
+ # prefer to the attribute-based syntax. This code using style:
246
+ #
247
+ # pdf.table(data) do
248
+ # style(row(0), :background_color => 'ff00ff')
249
+ # style(column(0)) { |c| c.border_width += 1 }
250
+ # end
251
+ #
252
+ # is equivalent to:
253
+ #
254
+ # pdf.table(data) do
255
+ # row(0).style :background_color => 'ff00ff'
256
+ # column(0).style { |c| c.border_width += 1 }
257
+ # end
258
+ #
259
+ def style(stylable, style_hash={}, &block)
260
+ stylable.style(style_hash, &block)
261
+ end
262
+
263
+ # Draws the table onto the document at the document's current y-position.
264
+ #
265
+ def draw
266
+ with_position do
267
+ # Reference bounds are the non-stretchy bounds used to decide when to
268
+ # flow to a new column / page.
269
+ ref_bounds = @pdf.reference_bounds
270
+
271
+ # Determine whether we're at the top of the current bounds (margin box or
272
+ # bounding box). If we're at the top, we couldn't gain any more room by
273
+ # breaking to the next page -- this means, in particular, that if the
274
+ # first row is taller than the margin box, we will only move to the next
275
+ # page if we're below the top. Some floating-point tolerance is added to
276
+ # the calculation.
277
+ #
278
+ # Note that we use the actual bounds, not the reference bounds. This is
279
+ # because even if we are in a stretchy bounding box, flowing to the next
280
+ # page will not buy us any space if we are at the top.
281
+ #
282
+ # initial_row_on_initial_page may return 0 (already at the top OR created
283
+ # a new page) or -1 (enough space)
284
+ started_new_page_at_row = initial_row_on_initial_page
285
+
286
+ # The cell y-positions are based on an infinitely long canvas. The offset
287
+ # keeps track of how much we have to add to the original, theoretical
288
+ # y-position to get to the actual position on the current page.
289
+ offset = @pdf.y
290
+
291
+ # Duplicate each cell of the header row into @header_row so it can be
292
+ # modified in before_rendering_page callbacks.
293
+ @header_row = header_rows if @header
294
+
295
+ # Track cells to be drawn on this page. They will all be drawn when this
296
+ # page is finished.
297
+ cells_this_page = []
298
+
299
+ @cells.each do |cell|
300
+ if start_new_page?(cell, offset, ref_bounds)
301
+ # draw cells on the current page and then start a new one
302
+ # this will also add a header to the new page if a header is set
303
+ # reset array of cells for the new page
304
+ cells_this_page, offset = ink_and_draw_cells_and_start_new_page(cells_this_page, cell)
305
+
306
+ # remember the current row for background coloring
307
+ started_new_page_at_row = cell.row
308
+ end
309
+
310
+ # Set background color, if any.
311
+ cell = set_background_color(cell, started_new_page_at_row)
312
+
313
+ # add the current cell to the cells array for the current page
314
+ cells_this_page << [cell, [cell.relative_x, cell.relative_y(offset)]]
315
+ end
316
+
317
+ # Draw the last page of cells
318
+ ink_and_draw_cells(cells_this_page)
319
+
320
+ @pdf.move_cursor_to(@cells.last.relative_y(offset) - @cells.last.height)
321
+ end
322
+ end
323
+
324
+ # Calculate and return the constrained column widths, taking into account
325
+ # each cell's min_width, max_width, and any user-specified constraints on
326
+ # the table or column size.
327
+ #
328
+ # Because the natural widths can be silly, this does not always work so well
329
+ # at guessing a good size for columns that have vastly different content. If
330
+ # you see weird problems like CannotFit errors or shockingly bad column
331
+ # sizes, you should specify more column widths manually.
332
+ #
333
+ def column_widths
334
+ @column_widths ||= begin
335
+ if width - cells.min_width < -Prawn::FLOAT_PRECISION
336
+ raise Errors::CannotFit,
337
+ "Table's width was set too small to contain its contents " +
338
+ "(min width #{cells.min_width}, requested #{width})"
339
+ end
340
+
341
+ if width - cells.max_width > Prawn::FLOAT_PRECISION
342
+ raise Errors::CannotFit,
343
+ "Table's width was set larger than its contents' maximum width " +
344
+ "(max width #{cells.max_width}, requested #{width})"
345
+ end
346
+
347
+ if width - natural_width < -Prawn::FLOAT_PRECISION
348
+ # Shrink the table to fit the requested width.
349
+ f = (width - cells.min_width).to_f / (natural_width - cells.min_width)
350
+
351
+ (0...column_length).map do |c|
352
+ min, nat = column(c).min_width, natural_column_widths[c]
353
+ (f * (nat - min)) + min
354
+ end
355
+ elsif width - natural_width > Prawn::FLOAT_PRECISION
356
+ # Expand the table to fit the requested width.
357
+ f = (width - cells.width).to_f / (cells.max_width - cells.width)
358
+
359
+ (0...column_length).map do |c|
360
+ nat, max = natural_column_widths[c], column(c).max_width
361
+ (f * (max - nat)) + nat
362
+ end
363
+ else
364
+ natural_column_widths
365
+ end
366
+ end
367
+ end
368
+
369
+ # Returns an array with the height of each row.
370
+ #
371
+ def row_heights
372
+ @natural_row_heights ||=
373
+ begin
374
+ heights_by_row = Hash.new(0)
375
+ cells.each do |cell|
376
+ next if cell.is_a?(Cell::SpanDummy)
377
+
378
+ # Split the height of row-spanned cells evenly by rows
379
+ height_per_row = cell.height.to_f / cell.rowspan
380
+ cell.rowspan.times do |i|
381
+ heights_by_row[cell.row + i] =
382
+ [heights_by_row[cell.row + i], height_per_row].max
383
+ end
384
+ end
385
+ heights_by_row.sort_by { |row, _| row }.map { |_, h| h }
386
+ end
387
+ end
388
+
389
+ protected
390
+
391
+ # sets the background color (if necessary) for the given cell
392
+ def set_background_color(cell, started_new_page_at_row)
393
+ if defined?(@row_colors) && @row_colors && (!@header || cell.row > 0)
394
+ # Ensure coloring restarts on every page (to make sure the header
395
+ # and first row of a page are not colored the same way).
396
+ rows = number_of_header_rows
397
+
398
+ index = cell.row - [started_new_page_at_row, rows].max
399
+
400
+ cell.background_color ||= @row_colors[index % @row_colors.length]
401
+ end
402
+ cell
403
+ end
404
+
405
+ # number of rows of the header
406
+ # @return [Integer] the number of rows of the header
407
+ def number_of_header_rows
408
+ # header may be set to any integer value -> number of rows
409
+ if @header.is_a? Integer
410
+ return @header
411
+ # header may be set to true -> first row is repeated
412
+ elsif @header
413
+ return 1
414
+ end
415
+ # defaults to 0 header rows
416
+ 0
417
+ end
418
+
419
+ # should we start a new page? (does the current row fail to fit on this page)
420
+ def start_new_page?(cell, offset, ref_bounds)
421
+ # we only need to run this test on the first cell in a row
422
+ # check if the rows height fails to fit on the page
423
+ # check if the row is not the first on that page (wouldn't make sense to go to next page in this case)
424
+ (cell.column == 0 && cell.row > 0 &&
425
+ !row(cell.row).fits_on_current_page?(offset, ref_bounds))
426
+ end
427
+
428
+ # ink cells and then draw them
429
+ def ink_and_draw_cells(cells_this_page, draw_cells = true)
430
+ ink_cells(cells_this_page)
431
+ Cell.draw_cells(cells_this_page) if draw_cells
432
+ end
433
+
434
+ # ink and draw cells, then start a new page
435
+ def ink_and_draw_cells_and_start_new_page(cells_this_page, cell)
436
+ # don't draw only a header
437
+ draw_cells = (@header_row.nil? || cells_this_page.size > @header_row.size)
438
+
439
+ ink_and_draw_cells(cells_this_page, draw_cells)
440
+
441
+ # start a new page or column
442
+ @pdf.bounds.move_past_bottom
443
+
444
+ offset = (@pdf.y - cell.y)
445
+
446
+ cells_next_page = []
447
+
448
+ header_height = add_header(cell.row, cells_next_page)
449
+
450
+ # account for header height in newly generated offset
451
+ offset -= header_height
452
+
453
+ # reset cells_this_page in calling function and return new offset
454
+ return cells_next_page, offset
455
+ end
456
+
457
+ # Ink all cells on the current page
458
+ def ink_cells(cells_this_page)
459
+ if defined?(@before_rendering_page) && @before_rendering_page
460
+ c = Cells.new(cells_this_page.map { |ci, _| ci })
461
+ @before_rendering_page.call(c)
462
+ end
463
+ end
464
+
465
+ # Determine whether we're at the top of the current bounds (margin box or
466
+ # bounding box). If we're at the top, we couldn't gain any more room by
467
+ # breaking to the next page -- this means, in particular, that if the
468
+ # first row is taller than the margin box, we will only move to the next
469
+ # page if we're below the top. Some floating-point tolerance is added to
470
+ # the calculation.
471
+ #
472
+ # Note that we use the actual bounds, not the reference bounds. This is
473
+ # because even if we are in a stretchy bounding box, flowing to the next
474
+ # page will not buy us any space if we are at the top.
475
+ # @return [Integer] 0 (already at the top OR created a new page) or -1 (enough space)
476
+ def initial_row_on_initial_page
477
+ # we're at the top of our bounds
478
+ return 0 if fits_on_page?(@pdf.bounds.height)
479
+
480
+ needed_height = row(0..number_of_header_rows).height
481
+
482
+ # have we got enough room to fit the first row (including header row(s))
483
+ use_reference_bounds = true
484
+ return -1 if fits_on_page?(needed_height, use_reference_bounds)
485
+
486
+ # If there isn't enough room left on the page to fit the first data row
487
+ # (including the header), start the table on the next page.
488
+ @pdf.bounds.move_past_bottom
489
+
490
+ # we are at the top of a new page
491
+ 0
492
+ end
493
+
494
+ # do we have enough room to fit a given height on to the current page?
495
+ def fits_on_page?(needed_height, use_reference_bounds = false)
496
+ if use_reference_bounds
497
+ bounds = @pdf.reference_bounds
498
+ else
499
+ bounds = @pdf.bounds
500
+ end
501
+ needed_height < @pdf.y - (bounds.absolute_bottom - Prawn::FLOAT_PRECISION)
502
+ end
503
+
504
+ # return the header rows
505
+ # @api private
506
+ def header_rows
507
+ header_rows = Cells.new
508
+ number_of_header_rows.times do |r|
509
+ row(r).each { |cell| header_rows[cell.row, cell.column] = cell.dup }
510
+ end
511
+ header_rows
512
+ end
513
+
514
+ # Converts the array of cellable objects given into instances of
515
+ # Prawn::Table::Cell, and sets up their in-table properties so that they
516
+ # know their own position in the table.
517
+ #
518
+ def make_cells(data, cell_style = {})
519
+ assert_proper_table_data(data)
520
+
521
+ cells = Cells.new
522
+
523
+ row_number = 0
524
+ data.each do |row_cells|
525
+ column_number = 0
526
+ row_cells.each do |cell_data|
527
+ # If we landed on a spanned cell (from a rowspan above), continue
528
+ # until we find an empty spot.
529
+ column_number += 1 until cells[row_number, column_number].nil?
530
+
531
+ # Build the cell and store it in the Cells collection.
532
+ cell = if cell_data.is_a?(Hash)
533
+ Cell.make(@pdf, cell_style.merge(cell_data))
534
+ else
535
+ Cell.make(@pdf, cell_data, cell_style)
536
+ end
537
+ cells[row_number, column_number] = cell
538
+
539
+ # Add dummy cells for the rest of the cells in the span group. This
540
+ # allows Prawn to keep track of the horizontal and vertical space
541
+ # occupied in each column and row spanned by this cell, while still
542
+ # leaving the master (top left) cell in the group responsible for
543
+ # drawing. Dummy cells do not put ink on the page.
544
+ cell.rowspan.times do |i|
545
+ cell.colspan.times do |j|
546
+ next if i == 0 && j == 0
547
+
548
+ # It is an error to specify spans that overlap; catch this here
549
+ if cells[row_number + i, column_number + j]
550
+ raise Prawn::Errors::InvalidTableSpan,
551
+ "Spans overlap at row #{row_number + i}, " +
552
+ "column #{column_number + j}."
553
+ end
554
+
555
+ dummy = Cell::SpanDummy.new(@pdf, cell)
556
+ cells[row_number + i, column_number + j] = dummy
557
+ cell.dummy_cells << dummy
558
+ end
559
+ end
560
+
561
+ column_number += cell.colspan
562
+ end
563
+
564
+ row_number += 1
565
+ end
566
+
567
+ # Calculate the number of rows and columns in the table, taking into
568
+ # account that some cells may span past the end of the physical cells we
569
+ # have.
570
+ @row_length = cells.map do |cell|
571
+ cell.row + cell.rowspan
572
+ end.max
573
+
574
+ @column_length = cells.map do |cell|
575
+ cell.column + cell.colspan
576
+ end.max
577
+
578
+ cells
579
+ end
580
+
581
+ def add_header(row_number, cells_this_page)
582
+ x_offset = @pdf.bounds.left_side - @pdf.bounds.absolute_left
583
+ header_height = 0
584
+ if row_number > 0 && @header
585
+ y_coord = @pdf.cursor
586
+ number_of_header_rows.times do |h|
587
+ additional_header_height = add_one_header_row(cells_this_page, x_offset, y_coord-header_height, row_number-1, h)
588
+ header_height += additional_header_height
589
+ end
590
+ end
591
+ header_height
592
+ end
593
+
594
+ # Add the header row(s) to the given array of cells at the given y-position.
595
+ # Number the row with the given +row+ index, so that the header appears (in
596
+ # any Cells built for this page) immediately prior to the first data row on
597
+ # this page.
598
+ #
599
+ # Return the height of the header.
600
+ #
601
+ def add_one_header_row(page_of_cells, x_offset, y, row, row_of_header=nil)
602
+ rows_to_operate_on = @header_row
603
+ rows_to_operate_on = @header_row.rows(row_of_header) if row_of_header
604
+ rows_to_operate_on.each do |cell|
605
+ cell.row = row
606
+ cell.dummy_cells.each {|c|
607
+ if cell.rowspan > 1
608
+ # be sure to account for cells that span multiple rows
609
+ # in this case you need multiple row numbers
610
+ c.row += row
611
+ else
612
+ c.row = row
613
+ end
614
+ }
615
+ page_of_cells << [cell, [cell.x + x_offset, y]]
616
+ end
617
+ rows_to_operate_on.height
618
+ end
619
+
620
+ # Raises an error if the data provided cannot be converted into a valid
621
+ # table.
622
+ #
623
+ def assert_proper_table_data(data)
624
+ if data.nil? || data.empty?
625
+ raise Prawn::Errors::EmptyTable,
626
+ "data must be a non-empty, non-nil, two dimensional array " +
627
+ "of cell-convertible objects"
628
+ end
629
+
630
+ unless data.all? { |e| Array === e }
631
+ raise Prawn::Errors::InvalidTableData,
632
+ "data must be a two dimensional array of cellable objects"
633
+ end
634
+ end
635
+
636
+ # Returns an array of each column's natural (unconstrained) width.
637
+ #
638
+ def natural_column_widths
639
+ @natural_column_widths ||= ColumnWidthCalculator.new(cells).natural_widths
640
+ end
641
+
642
+ # Returns the "natural" (unconstrained) width of the table. This may be
643
+ # extremely silly; for example, the unconstrained width of a paragraph of
644
+ # text is the width it would assume if it were not wrapped at all. Could be
645
+ # a mile long.
646
+ #
647
+ def natural_width
648
+ @natural_width ||= natural_column_widths.inject(0, &:+)
649
+ end
650
+
651
+ # Assigns the calculated column widths to each cell. This ensures that each
652
+ # cell in a column is the same width. After this method is called,
653
+ # subsequent calls to column_widths and width should return the finalized
654
+ # values that will be used to ink the table.
655
+ #
656
+ def set_column_widths
657
+ column_widths.each_with_index do |w, col_num|
658
+ column(col_num).width = w
659
+ end
660
+ end
661
+
662
+ # Assigns the row heights to each cell. This ensures that every cell in a
663
+ # row is the same height.
664
+ #
665
+ def set_row_heights
666
+ row_heights.each_with_index { |h, row_num| row(row_num).height = h }
667
+ end
668
+
669
+ # Set each cell's position based on the widths and heights of cells
670
+ # preceding it.
671
+ #
672
+ def position_cells
673
+ # Calculate x- and y-positions as running sums of widths / heights.
674
+ x_positions = column_widths.inject([0]) { |ary, x|
675
+ ary << (ary.last + x); ary }[0..-2]
676
+ x_positions.each_with_index { |x, i| column(i).x = x }
677
+
678
+ # y-positions assume an infinitely long canvas starting at zero -- this
679
+ # is corrected for in Table#draw, and page breaks are properly inserted.
680
+ y_positions = row_heights.inject([0]) { |ary, y|
681
+ ary << (ary.last - y); ary}[0..-2]
682
+ y_positions.each_with_index { |y, i| row(i).y = y }
683
+ end
684
+
685
+ # Sets up a bounding box to position the table according to the specified
686
+ # :position option, and yields.
687
+ #
688
+ def with_position
689
+ x = case defined?(@position) && @position || :left
690
+ when :left then return yield
691
+ when :center then (@pdf.bounds.width - width) / 2.0
692
+ when :right then @pdf.bounds.width - width
693
+ when Numeric then @position
694
+ else raise ArgumentError, "unknown position #{@position.inspect}"
695
+ end
696
+ dy = @pdf.bounds.absolute_top - @pdf.y
697
+ final_y = nil
698
+
699
+ @pdf.bounding_box([x, @pdf.bounds.top], :width => width) do
700
+ @pdf.move_down dy
701
+ yield
702
+ final_y = @pdf.y
703
+ end
704
+
705
+ @pdf.y = final_y
706
+ end
707
+
708
+ end
709
+ end
710
+
711
+ Prawn::Document.extensions << Prawn::Table::Interface