lib/prawn/table.rb in prawn-1.0.0.rc1 vs lib/prawn/table.rb in prawn-1.0.0.rc2
- old
+ new
@@ -10,10 +10,11 @@
require 'prawn/table/cell'
require 'prawn/table/cell/in_table'
require 'prawn/table/cell/text'
require 'prawn/table/cell/subtable'
require 'prawn/table/cell/image'
+require 'prawn/table/cell/span_dummy'
module Prawn
class Document
@@ -153,10 +154,24 @@
# Position (:left, :right, :center, or a number indicating distance in
# points from the left edge) of the table within its parent bounds.
#
attr_writer :position
+ # Returns a Prawn::Table::Cells object representing all of the cells in
+ # this table.
+ #
+ attr_reader :cells
+
+ # Specify a callback to be called before each page of cells is rendered.
+ # The block is passed a Cells object containing all cells to be rendered on
+ # that page. You can change styling of the cells in this block, but keep in
+ # mind that the cells have already been positioned and sized.
+ #
+ def before_rendering_page(&block)
+ @before_rendering_page = block
+ end
+
# Returns the width of the table in PDF points.
#
def width
@width ||= [natural_width, @pdf.bounds.width].min
end
@@ -268,25 +283,40 @@
offset = @pdf.y
started_new_page_at_row = 0
end
end
+ # Duplicate each cell of the header row into @header_row so it can be
+ # modified in before_rendering_page callbacks.
+ if @header
+ @header_row = Cells.new
+ row(0).each { |cell| @header_row[cell.row, cell.column] = cell.dup }
+ end
+
# Track cells to be drawn on this page. They will all be drawn when this
# page is finished.
cells_this_page = []
@cells.each do |cell|
if cell.height > (cell.y + offset) - ref_bounds.absolute_bottom &&
cell.row > started_new_page_at_row
# Ink all cells on the current page
+ if @before_rendering_page
+ c = Cells.new(cells_this_page.map { |c, _| c })
+ @before_rendering_page.call(c)
+ end
Cell.draw_cells(cells_this_page)
cells_this_page = []
# start a new page or column
@pdf.bounds.move_past_bottom
- draw_header unless cell.row == 0
- offset = @pdf.y - cell.y
+ if cell.row > 0 && @header
+ header_height = add_header(cells_this_page, @pdf.cursor, cell.row-1)
+ else
+ header_height = 0
+ end
+ offset = @pdf.y - cell.y - header_height
started_new_page_at_row = cell.row
end
# Don't modify cell.x / cell.y here, as we want to reuse the original
# values when re-inking the table. #draw should be able to be called
@@ -299,18 +329,24 @@
x += @pdf.bounds.left_side - @pdf.bounds.absolute_left
y -= @pdf.bounds.absolute_bottom
# Set background color, if any.
if @row_colors && (!@header || cell.row > 0)
- index = @header ? (cell.row - 1) : cell.row
+ # Ensure coloring restarts on every page (to make sure the header
+ # and first row of a page are not colored the same way).
+ index = cell.row - [started_new_page_at_row, @header ? 1 : 0].max
cell.background_color ||= @row_colors[index % @row_colors.length]
end
cells_this_page << [cell, [x, y]]
last_y = y
end
# Draw the last page of cells
+ if @before_rendering_page
+ c = Cells.new(cells_this_page.map { |c, _| c })
+ @before_rendering_page.call(c)
+ end
Cell.draw_cells(cells_this_page)
@pdf.move_cursor_to(last_y - @cells.last.height)
end
end
@@ -341,19 +377,19 @@
if width - natural_width < -epsilon
# Shrink the table to fit the requested width.
f = (width - cells.min_width).to_f / (natural_width - cells.min_width)
(0...column_length).map do |c|
- min, nat = column(c).min_width, column(c).width
+ min, nat = column(c).min_width, natural_column_widths[c]
(f * (nat - min)) + min
end
elsif width - natural_width > epsilon
# Expand the table to fit the requested width.
f = (width - cells.width).to_f / (cells.max_width - cells.width)
(0...column_length).map do |c|
- nat, max = column(c).width, column(c).max_width
+ nat, max = natural_column_widths[c], column(c).max_width
(f * (max - nat)) + nat
end
else
natural_column_widths
end
@@ -361,11 +397,25 @@
end
# Returns an array with the height of each row.
#
def row_heights
- @natural_row_heights ||= (0...row_length).map{ |r| row(r).height }
+ @natural_row_heights ||=
+ begin
+ heights_by_row = Hash.new(0)
+ cells.each do |cell|
+ next if cell.is_a?(Cell::SpanDummy)
+
+ # Split the height of row-spanned cells evenly by rows
+ height_per_row = cell.height.to_f / cell.rowspan
+ cell.rowspan.times do |i|
+ heights_by_row[cell.row + i] =
+ [heights_by_row[cell.row + i], height_per_row].max
+ end
+ end
+ heights_by_row.sort_by { |row, _| row }.map { |_, h| h }
+ end
end
protected
# Converts the array of cellable objects given into instances of
@@ -373,27 +423,81 @@
# know their own position in the table.
#
def make_cells(data)
assert_proper_table_data(data)
- cells = []
+ cells = Cells.new
- @row_length = data.length
- @column_length = data.map{ |r| r.length }.max
+ row_number = 0
+ data.each do |row_cells|
+ column_number = 0
+ row_cells.each do |cell_data|
+ # If we landed on a spanned cell (from a rowspan above), continue
+ # until we find an empty spot.
+ column_number += 1 until cells[row_number, column_number].nil?
- data.each_with_index do |row_cells, row_number|
- row_cells.each_with_index do |cell_data, column_number|
+ # Build the cell and store it in the Cells collection.
cell = Cell.make(@pdf, cell_data)
- cell.extend(Cell::InTable)
- cell.row = row_number
- cell.column = column_number
- cells << cell
+ cells[row_number, column_number] = cell
+
+ # Add dummy cells for the rest of the cells in the span group. This
+ # allows Prawn to keep track of the horizontal and vertical space
+ # occupied in each column and row spanned by this cell, while still
+ # leaving the master (top left) cell in the group responsible for
+ # drawing. Dummy cells do not put ink on the page.
+ cell.rowspan.times do |i|
+ cell.colspan.times do |j|
+ next if i == 0 && j == 0
+
+ # It is an error to specify spans that overlap; catch this here
+ if bad_cell = cells[row_number + i, column_number + j]
+ raise Prawn::Errors::InvalidTableSpan,
+ "Spans overlap at row #{row_number + i}, " +
+ "column #{column_number + j}."
+ end
+
+ dummy = Cell::SpanDummy.new(@pdf, cell)
+ cells[row_number + i, column_number + j] = dummy
+ cell.dummy_cells << dummy
+ end
+ end
+
+ column_number += cell.colspan
end
+
+ row_number += 1
end
+
+ # Calculate the number of rows and columns in the table, taking into
+ # account that some cells may span past the end of the physical cells we
+ # have.
+ @row_length = cells.map do |cell|
+ cell.row + cell.rowspan
+ end.max
+
+ @column_length = cells.map do |cell|
+ cell.column + cell.colspan
+ end.max
+
cells
end
+ # Add the header row to the given array of cells at the given y-position.
+ # Number the row with the given +row+ index, so that the header appears (in
+ # any Cells built for this page) immediately prior to the first data row on
+ # this page.
+ #
+ # Return the height of the header.
+ #
+ def add_header(page_of_cells, y, row)
+ @header_row.each do |cell|
+ cell.row = row
+ page_of_cells << [cell, [cell.x, y]]
+ end
+ @header_row.height
+ end
+
# Raises an error if the data provided cannot be converted into a valid
# table.
#
def assert_proper_table_data(data)
if data.nil? || data.empty?
@@ -406,35 +510,36 @@
raise Prawn::Errors::InvalidTableData,
"data must be a two dimensional array of cellable objects"
end
end
- # If the table has a header, draw it at the current position.
- #
- def draw_header
- if @header
- y = @pdf.cursor
- row(0).each do |cell|
- cell.y = y
- cell.draw
- end
- @pdf.move_cursor_to(y - row(0).height)
- end
- end
-
# Returns an array of each column's natural (unconstrained) width.
#
def natural_column_widths
- @natural_column_widths ||= (0...column_length).map { |c| column(c).width }
+ @natural_column_widths ||=
+ begin
+ widths_by_column = Hash.new(0)
+ cells.each do |cell|
+ next if cell.is_a?(Cell::SpanDummy)
+
+ # Split the width of colspanned cells evenly by columns
+ width_per_column = cell.width.to_f / cell.colspan
+ cell.colspan.times do |i|
+ widths_by_column[cell.column + i] =
+ [widths_by_column[cell.column + i], width_per_column].max
+ end
+ end
+ widths_by_column.sort_by { |col, _| col }.map { |_, w| w }
+ end
end
# Returns the "natural" (unconstrained) width of the table. This may be
# extremely silly; for example, the unconstrained width of a paragraph of
# text is the width it would assume if it were not wrapped at all. Could be
# a mile long.
#
def natural_width
- @natural_width ||= natural_column_widths.inject(0) { |sum, w| sum + w }
+ @natural_width ||= natural_column_widths.inject(0, &:+)
end
# Assigns the calculated column widths to each cell. This ensures that each
# cell in a column is the same width. After this method is called,
# subsequent calls to column_widths and width should return the finalized