lib/prawn/table.rb in prawn-0.15.0 vs lib/prawn/table.rb in prawn-1.0.0.rc1

- old
+ new

@@ -4,25 +4,21 @@ # # Copyright December 2009, Brad Ediger. All rights reserved. # # This is free software. Please see the LICENSE and COPYING files for details. -require_relative 'table/column_width_calculator' -require_relative 'table/cells' -require_relative 'table/cell' -require_relative 'table/cell/in_table' -require_relative 'table/cell/text' -require_relative 'table/cell/subtable' -require_relative 'table/cell/image' -require_relative 'table/cell/span_dummy' +require 'prawn/table/cells' +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' module Prawn class Document - - # @group Experimental API - + # Set up and draw a table on this document. A block can be given, which will # be run after cell setup but before layout and drawing. # # See the documentation on Prawn::Table for details on the arguments. # @@ -52,11 +48,11 @@ # # String:: # Produces a text cell. This is the most common usage. # Prawn::Table::Cell:: # If you have already built a Cell or have a custom subclass of Cell you - # want to use in a table, you can pass through Cell objects. + # want to use in a table, you can pass through Cell objects. # Prawn::Table:: # Creates a subtable (a table within a cell). You can use # Prawn::Document#make_table to create a table for use as a subtable # without immediately drawing it. See examples/table/bill.rb for a # somewhat complex use of subtables. @@ -74,16 +70,16 @@ # # +cell_style+:: # A hash of style options to style all cells. See the documentation on # Prawn::Table::Cell for all cell style options. # +header+:: - # If set to +true+, the first row will be repeated on every page. If set - # to an Integer, the first +x+ rows will be repeated on every page. Row - # numbering (for styling and other row-specific options) always indexes - # based on your data array. Whether or not you have a header, row(n) always - # refers to the nth element (starting from 0) of the +data+ array. - # +column_widths+:: + # If set to +true+, the first row will be repeated on every page. The + # header must be included as the first row of your data. Row numbering + # (for styling and other row-specific options) always indexes based on + # your data array. Whether or not you have a header, row(n) always refers + # to the nth element (starting from 0) of the +data+ array. + # +column_widths+:: # Sets widths for individual columns. Manually setting widths can give # better results than letting Prawn guess at them, as Prawn's algorithm # for defaulting widths is currently pretty boneheaded. If you experience # problems like weird column widths or CannotFit errors, try manually # setting widths on more columns. @@ -102,20 +98,20 @@ # inch (72 pt) wide: # # pdf.table(data) do |table| # table.rows(1..3).width = 72 # end - # + # # As with Prawn::Document#initialize, if the block has no arguments, it will # be evaluated in the context of the object itself. The above code could be # rewritten as: # # pdf.table(data) do # rows(1..3).width = 72 # end # - class Table + class Table # Set up a table on the given document. Arguments: # # +data+:: # A two-dimensional array of cell-like objects. See the "Data" section @@ -138,11 +134,11 @@ end set_column_widths set_row_heights position_cells - end + end # Number of rows in the table. # attr_reader :row_length @@ -157,36 +153,22 @@ # 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 # Sets column widths for the table. The argument can be one of the following # types: # - # +Array+:: + # +Array+:: # <tt>[w0, w1, w2, ...]</tt> (specify a width for each column) - # +Hash+:: + # +Hash+:: # <tt>{0 => w0, 1 => w1, ...}</tt> (keys are column names, values are # widths) # +Numeric+:: # +72+ (sets width for all columns) # @@ -208,13 +190,12 @@ def height cells.height end # If +true+, designates the first row as a header row to be repeated on - # every page. If an integer, designates the number of rows to be treated - # as a header Does not change row numbering -- row numbers always index - # into the data array provided, with no modification. + # every page. Does not change row numbering -- row numbers always index into + # the data array provided, with no modification. # attr_writer :header # Accepts an Array of alternating row colors to stripe the table. # @@ -279,108 +260,57 @@ started_new_page_at_row = -1 # 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 + needed_height += row(1).height if @header 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 - # 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 - # 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 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) cells_this_page = [] # 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 + draw_header unless cell.row == 0 + offset = @pdf.y - cell.y 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 + y += offset - # Translate coordinates to the bounds we are in, since drawing is + # 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 - + if @row_colors && (!@header || cell.row > 0) + index = @header ? (cell.row - 1) : cell.row 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 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) @pdf.move_cursor_to(last_y - @cells.last.height) end end @@ -411,19 +341,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, natural_column_widths[c] + min, nat = column(c).min_width, column(c).width (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 = natural_column_widths[c], column(c).max_width + nat, max = column(c).width, column(c).max_width (f * (max - nat)) + nat end else natural_column_widths end @@ -431,25 +361,11 @@ end # Returns an array with the height of each row. # def row_heights - @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 + @natural_row_heights ||= (0...row_length).map{ |r| row(r).height } end protected # Converts the array of cellable objects given into instances of @@ -457,84 +373,27 @@ # know their own position in the table. # def make_cells(data) assert_proper_table_data(data) - cells = Cells.new + cells = [] + + @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? - - # Build the cell and store it in the Cells collection. + data.each_with_index do |row_cells, row_number| + row_cells.each_with_index do |cell_data, column_number| cell = Cell.make(@pdf, cell_data) - 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 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 + cell.extend(Cell::InTable) + cell.row = row_number + cell.column = column_number + cells << cell 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(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) - 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 } - page_of_cells << [cell, [cell.x + x_offset, y]] - end - rows_to_operate_on.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? @@ -547,32 +406,45 @@ 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 ||= ColumnWidthCalculator.new(cells).natural_widths + @natural_column_widths ||= (0...column_length).map { |c| column(c).width } 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, &:+) + @natural_width ||= natural_column_widths.inject(0) { |sum, w| sum + w } 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 # values that will be used to ink the table. # def set_column_widths - column_widths.each_with_index do |w, col_num| + column_widths.each_with_index do |w, col_num| column(col_num).width = w end end # Assigns the row heights to each cell. This ensures that every cell in a @@ -585,11 +457,11 @@ # Set each cell's position based on the widths and heights of cells # preceding it. # def position_cells # Calculate x- and y-positions as running sums of widths / heights. - x_positions = column_widths.inject([0]) { |ary, x| + x_positions = column_widths.inject([0]) { |ary, x| ary << (ary.last + x); ary }[0..-2] x_positions.each_with_index { |x, i| column(i).x = x } # y-positions assume an infinitely long canvas starting at zero -- this # is corrected for in Table#draw, and page breaks are properly inserted. @@ -600,10 +472,10 @@ # Sets up a bounding box to position the table according to the specified # :position option, and yields. # def with_position - x = case defined?(@position) && @position || :left + x = case @position || :left when :left then return yield when :center then (@pdf.bounds.width - width) / 2.0 when :right then @pdf.bounds.width - width when Numeric then @position else raise ArgumentError, "unknown position #{@position.inspect}"