lib/prawn/table.rb in prawn-1.0.0.rc2 vs lib/prawn/table.rb in prawn-1.0.0

- old
+ new

@@ -4,22 +4,25 @@ # # Copyright December 2009, Brad Ediger. All rights reserved. # # This is free software. Please see the LICENSE and COPYING files for details. -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' -require 'prawn/table/cell/span_dummy' +require_relative 'table/column_width_calculator' +require_relative 'table/cell' +require_relative 'table/cells' +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' 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. # @@ -49,11 +52,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. @@ -71,16 +74,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. 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+:: + # 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+:: # 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. @@ -99,20 +102,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 @@ -135,11 +138,11 @@ end set_column_widths set_row_heights position_cells - end + end # Number of rows in the table. # attr_reader :row_length @@ -163,11 +166,11 @@ # 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. @@ -177,13 +180,13 @@ 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) # @@ -205,12 +208,13 @@ def height cells.height end # If +true+, designates the first row as a header row to be repeated on - # every page. Does not change row numbering -- row numbers always index into - # the data array provided, with no modification. + # 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. # attr_writer :header # Accepts an Array of alternating row colors to stripe the table. # @@ -275,11 +279,17 @@ 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 - needed_height += row(1).height if @header + 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 @@ -287,64 +297,88 @@ # 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 } + 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 @before_rendering_page - c = Cells.new(cells_this_page.map { |c, _| c }) + 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 - header_height = add_header(cells_this_page, @pdf.cursor, cell.row-1) + 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 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 @row_colors && (!@header || cell.row > 0) + 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). - index = cell.row - [started_new_page_at_row, @header ? 1 : 0].max + 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.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 }) + 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) @@ -424,11 +458,11 @@ # def make_cells(data) assert_proper_table_data(data) cells = Cells.new - + 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 @@ -447,11 +481,11 @@ 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] + if cells[row_number + i, column_number + j] raise Prawn::Errors::InvalidTableSpan, "Spans overlap at row #{row_number + i}, " + "column #{column_number + j}." end @@ -479,23 +513,26 @@ end.max cells end - # Add the header row to the given array of cells at the given y-position. + # 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, y, row) - @header_row.each do |cell| + 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 - page_of_cells << [cell, [cell.x, y]] + cell.dummy_cells.each {|c| c.row = row } + page_of_cells << [cell, [cell.x + x_offset, y]] end - @header_row.height + rows_to_operate_on.height end # Raises an error if the data provided cannot be converted into a valid # table. # @@ -513,25 +550,11 @@ end # Returns an array of each column's natural (unconstrained) width. # def natural_column_widths - @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 + @natural_column_widths ||= ColumnWidthCalculator.new(cells).natural_widths 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 @@ -545,11 +568,11 @@ # 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 @@ -562,11 +585,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. @@ -577,10 +600,10 @@ # Sets up a bounding box to position the table according to the specified # :position option, and yields. # def with_position - x = case @position || :left + x = case defined?(@position) && @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}"