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