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}"