prawn-table 0.0.1

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