prawn-table 0.0.1

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