lib/prawn/table.rb in prawn-table-0.1.0 vs lib/prawn/table.rb in prawn-table-0.1.1

- old
+ new

@@ -135,11 +135,10 @@ # def initialize(data, document, options={}, &block) @pdf = document @cells = make_cells(data) @header = false - @epsilon = 1e-9 options.each { |k, v| send("#{k}=", v) } if block block.arity < 1 ? instance_eval(&block) : block[self] end @@ -256,147 +255,64 @@ # Draws the table onto the document at the document's current y-position. # def draw with_position do - # The cell y-positions are based on an infinitely long canvas. The offset - # keeps track of how much we have to add to the original, theoretical - # y-position to get to the actual position on the current page. - offset = @pdf.y - # Reference bounds are the non-stretchy bounds used to decide when to # flow to a new column / page. ref_bounds = @pdf.reference_bounds - last_y = @pdf.y - # Determine whether we're at the top of the current bounds (margin box or # bounding box). If we're at the top, we couldn't gain any more room by # breaking to the next page -- this means, in particular, that if the # first row is taller than the margin box, we will only move to the next # page if we're below the top. Some floating-point tolerance is added to # the calculation. # # Note that we use the actual bounds, not the reference bounds. This is # because even if we are in a stretchy bounding box, flowing to the next # page will not buy us any space if we are at the top. - if @pdf.y > @pdf.bounds.height + @pdf.bounds.absolute_bottom - 0.001 - # we're at the top of our bounds - started_new_page_at_row = 0 - else - started_new_page_at_row = -1 + # + # initial_row_on_initial_page may return 0 (already at the top OR created + # a new page) or -1 (enough space) + started_new_page_at_row = initial_row_on_initial_page - # If there isn't enough room left on the page to fit the first data row - # (excluding the header), start the table on the next page. - needed_height = row(0).height - if @header - if @header.is_a? Integer - needed_height += row(1..@header).height - else - needed_height += row(1).height - end - end - if needed_height > @pdf.y - ref_bounds.absolute_bottom - @pdf.bounds.move_past_bottom - offset = @pdf.y - started_new_page_at_row = 0 - end - end + # The cell y-positions are based on an infinitely long canvas. The offset + # keeps track of how much we have to add to the original, theoretical + # y-position to get to the actual position on the current page. + offset = @pdf.y # 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 - if @header.is_a? Integer - @header.times do |r| - row(r).each { |cell| @header_row[cell.row, cell.column] = cell.dup } - end - else - row(0).each { |cell| @header_row[cell.row, cell.column] = cell.dup } - end - end + @header_row = header_rows if @header # 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| - # we only need to run this test on the first cell in a row - # check if the rows height fits on the page - # check if the row is not the first on that page (wouldn't make sense to go to next page in this case) - if cell.column == 0 && - row(cell.row).height_with_span > (cell.y + offset) - ref_bounds.absolute_bottom && - cell.row > started_new_page_at_row - # Ink all cells on the current page - if defined?(@before_rendering_page) && @before_rendering_page - c = Cells.new(cells_this_page.map { |ci, _| ci }) - @before_rendering_page.call(c) - end - if @header_row.nil? || cells_this_page.size > @header_row.size - Cell.draw_cells(cells_this_page) - end - cells_this_page = [] + if start_new_page?(cell, offset, ref_bounds) + # draw cells on the current page and then start a new one + # this will also add a header to the new page if a header is set + # reset array of cells for the new page + cells_this_page, offset = ink_and_draw_cells_and_start_new_page(cells_this_page, cell) - # start a new page or column - @pdf.bounds.move_past_bottom - x_offset = @pdf.bounds.left_side - @pdf.bounds.absolute_left - if cell.row > 0 && @header - if @header.is_a? Integer - header_height = 0 - y_coord = @pdf.cursor - @header.times do |h| - additional_header_height = add_header(cells_this_page, x_offset, y_coord-header_height, cell.row-1, h) - header_height += additional_header_height - end - else - header_height = add_header(cells_this_page, x_offset, @pdf.cursor, cell.row-1) - end - else - header_height = 0 - end - offset = @pdf.y - cell.y - header_height + # remember the current row for background coloring 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 - # multiple times. - x, y = cell.x, cell.y - y += offset - - # Translate coordinates to the bounds we are in, since drawing is - # relative to the cursor, not ref_bounds. - x += @pdf.bounds.left_side - @pdf.bounds.absolute_left - y -= @pdf.bounds.absolute_bottom - # Set background color, if any. - if defined?(@row_colors) && @row_colors && (!@header || cell.row > 0) - # Ensure coloring restarts on every page (to make sure the header - # and first row of a page are not colored the same way). - if @header.is_a? Integer - rows = @header - elsif @header - rows = 1 - else - rows = 0 - end - index = cell.row - [started_new_page_at_row, rows].max + cell = set_background_color(cell, started_new_page_at_row) - cell.background_color ||= @row_colors[index % @row_colors.length] - end - - cells_this_page << [cell, [x, y]] - last_y = y + # add the current cell to the cells array for the current page + cells_this_page << [cell, [cell.relative_x, cell.relative_y(offset)]] end + # Draw the last page of cells - if defined?(@before_rendering_page) && @before_rendering_page - c = Cells.new(cells_this_page.map { |ci, _| ci }) - @before_rendering_page.call(c) - end - Cell.draw_cells(cells_this_page) + ink_and_draw_cells(cells_this_page) - @pdf.move_cursor_to(last_y - @cells.last.height) + @pdf.move_cursor_to(@cells.last.relative_y(offset) - @cells.last.height) end end # Calculate and return the constrained column widths, taking into account # each cell's min_width, max_width, and any user-specified constraints on @@ -407,31 +323,31 @@ # you see weird problems like CannotFit errors or shockingly bad column # sizes, you should specify more column widths manually. # def column_widths @column_widths ||= begin - if width - cells.min_width < -epsilon + if width - cells.min_width < -Prawn::FLOAT_PRECISION raise Errors::CannotFit, "Table's width was set too small to contain its contents " + "(min width #{cells.min_width}, requested #{width})" end - if width - cells.max_width > epsilon + if width - cells.max_width > Prawn::FLOAT_PRECISION raise Errors::CannotFit, "Table's width was set larger than its contents' maximum width " + "(max width #{cells.max_width}, requested #{width})" end - if width - natural_width < -epsilon + if width - natural_width < -Prawn::FLOAT_PRECISION # 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, natural_column_widths[c] (f * (nat - min)) + min end - elsif width - natural_width > epsilon + elsif width - natural_width > Prawn::FLOAT_PRECISION # 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 = natural_column_widths[c], column(c).max_width @@ -462,11 +378,128 @@ heights_by_row.sort_by { |row, _| row }.map { |_, h| h } end end protected + + # sets the background color (if necessary) for the given cell + def set_background_color(cell, started_new_page_at_row) + if defined?(@row_colors) && @row_colors && (!@header || cell.row > 0) + # Ensure coloring restarts on every page (to make sure the header + # and first row of a page are not colored the same way). + rows = number_of_header_rows + index = cell.row - [started_new_page_at_row, rows].max + + cell.background_color ||= @row_colors[index % @row_colors.length] + end + cell + end + + # number of rows of the header + # @return [Integer] the number of rows of the header + def number_of_header_rows + # header may be set to any integer value -> number of rows + if @header.is_a? Integer + return @header + # header may be set to true -> first row is repeated + elsif @header + return 1 + end + # defaults to 0 header rows + 0 + end + + # should we start a new page? (does the current row fail to fit on this page) + def start_new_page?(cell, offset, ref_bounds) + # we only need to run this test on the first cell in a row + # check if the rows height fails to fit on the page + # check if the row is not the first on that page (wouldn't make sense to go to next page in this case) + (cell.column == 0 && cell.row > 0 && + !row(cell.row).fits_on_current_page?(offset, ref_bounds)) + end + + # ink cells and then draw them + def ink_and_draw_cells(cells_this_page, draw_cells = true) + ink_cells(cells_this_page) + Cell.draw_cells(cells_this_page) if draw_cells + end + + # ink and draw cells, then start a new page + def ink_and_draw_cells_and_start_new_page(cells_this_page, cell) + # don't draw only a header + draw_cells = (@header_row.nil? || cells_this_page.size > @header_row.size) + + ink_and_draw_cells(cells_this_page, draw_cells) + + # start a new page or column + @pdf.bounds.move_past_bottom + + offset = (@pdf.y - cell.y) + + cells_next_page = [] + + header_height = add_header(cell.row, cells_next_page) + + # account for header height in newly generated offset + offset -= header_height + + # reset cells_this_page in calling function and return new offset + return cells_next_page, offset + end + + # Ink all cells on the current page + def ink_cells(cells_this_page) + if defined?(@before_rendering_page) && @before_rendering_page + c = Cells.new(cells_this_page.map { |ci, _| ci }) + @before_rendering_page.call(c) + end + end + + # Determine whether we're at the top of the current bounds (margin box or + # bounding box). If we're at the top, we couldn't gain any more room by + # breaking to the next page -- this means, in particular, that if the + # first row is taller than the margin box, we will only move to the next + # page if we're below the top. Some floating-point tolerance is added to + # the calculation. + # + # Note that we use the actual bounds, not the reference bounds. This is + # because even if we are in a stretchy bounding box, flowing to the next + # page will not buy us any space if we are at the top. + # @return [Integer] 0 (already at the top OR created a new page) or -1 (enough space) + def initial_row_on_initial_page + # we're at the top of our bounds + return 0 if fits_on_page?(@pdf.bounds.height) + + needed_height = row(0..number_of_header_rows).height + + # have we got enough room to fit the first row (including header row(s)) + return -1 if fits_on_page?(needed_height) + + # If there isn't enough room left on the page to fit the first data row + # (including the header), start the table on the next page. + @pdf.bounds.move_past_bottom + + # we are at the top of a new page + 0 + end + + # do we have enough room to fit a given height on to the current page? + def fits_on_page?(needed_height) + needed_height < @pdf.y - (@pdf.bounds.absolute_bottom - Prawn::FLOAT_PRECISION) + end + + # return the header rows + # @api private + def header_rows + header_rows = Cells.new + number_of_header_rows.times do |r| + row(r).each { |cell| header_rows[cell.row, cell.column] = cell.dup } + end + header_rows + end + # Converts the array of cellable objects given into instances of # Prawn::Table::Cell, and sets up their in-table properties so that they # know their own position in the table. # def make_cells(data) @@ -526,23 +559,44 @@ end.max cells end + def add_header(row_number, cells_this_page) + x_offset = @pdf.bounds.left_side - @pdf.bounds.absolute_left + header_height = 0 + if row_number > 0 && @header + y_coord = @pdf.cursor + number_of_header_rows.times do |h| + additional_header_height = add_one_header_row(cells_this_page, x_offset, y_coord-header_height, row_number-1, h) + header_height += additional_header_height + end + end + header_height + end + # Add the header row(s) 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, x_offset, y, row, row_of_header=nil) + def add_one_header_row(page_of_cells, x_offset, y, row, row_of_header=nil) rows_to_operate_on = @header_row rows_to_operate_on = @header_row.rows(row_of_header) if row_of_header rows_to_operate_on.each do |cell| cell.row = row - cell.dummy_cells.each {|c| c.row = row + c.row } + cell.dummy_cells.each {|c| + if cell.rowspan > 1 + # be sure to account for cells that span multiple rows + # in this case you need multiple row numbers + c.row += row + else + c.row = row + end + } page_of_cells << [cell, [cell.x + x_offset, y]] end rows_to_operate_on.height end @@ -632,14 +686,9 @@ end @pdf.y = final_y end - private - - def epsilon - @epsilon - end end end Prawn::Document.extensions << Prawn::Table::Interface