Commit 7aa6f30c authored by Hartwig Brandl's avatar Hartwig Brandl
Browse files

optionally allow text of a final row of a page to be split to the next page

parent 63534f99
......@@ -15,6 +15,8 @@ require_relative 'table/cell/text'
require_relative 'table/cell/subtable'
require_relative 'table/cell/image'
require_relative 'table/cell/span_dummy'
require_relative 'table_splittable'
module Prawn
module Errors
......@@ -107,7 +109,7 @@ module Prawn
# See the documentation on Prawn::Table for details on the arguments.
#
def table(data, options={}, &block)
t = Table.new(data, self, options, &block)
t = make_table(data, options, &block)
t.draw
t
end
......@@ -118,7 +120,11 @@ module Prawn
# See the documentation on Prawn::Table for details on the arguments.
#
def make_table(data, options={}, &block)
Table.new(data, self, options, &block)
if options[:split_cells_across_pages]
TableSplittable.new(data, self, options, &block)
else
Table.new(data, self, options, &block)
end
end
end
......@@ -285,7 +291,21 @@ module Prawn
# modified in before_rendering_page callbacks.
@header_row = header_rows if @header
# Track cells to be drawn on this page. They will all be drawn when this
cells_this_page, offset = process_cells(ref_bounds, started_new_page_at_row, offset)
# Draw the last page of cells
ink_and_draw_cells(cells_this_page)
@pdf.move_cursor_to(@cells.last.relative_y(offset) - @cells.last.height)
end
end
# process cells into cells_this_page array
# this function is overwritten in TableSplittable
# if you change any code here be sure to check if that function
# needs to be adapted too
def process_cells(ref_bounds, started_new_page_at_row, offset)
# Track cells to be drawn on this page. They will all be drawn when this
# page is finished.
cells_this_page = []
......@@ -307,11 +327,7 @@ module Prawn
cells_this_page << [cell, [cell.relative_x, cell.relative_y(offset)]]
end
# Draw the last page of cells
ink_and_draw_cells(cells_this_page)
@pdf.move_cursor_to(@cells.last.relative_y(offset) - @cells.last.height)
end
return cells_this_page, offset
end
# Calculate and return the constrained column widths, taking into account
......@@ -410,27 +426,30 @@ module Prawn
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
def start_new_page?(cell, offset, ref_bounds, allow_first_row=false)
# we need to run it on every column to ensure it won't break on rowspans
# 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 &&
((cell.row > 0 || allow_first_row) &&
!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)
# split_cells and offset are only used in TableSplittable, used here to allow for function overload
def ink_and_draw_cells_and_start_new_page(cells_this_page, cell, split_cells=false, offset=false)
# 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
......
......@@ -56,6 +56,13 @@ module Prawn
#
attr_reader :padding
# content for the second cell of a cell that has been split (the one on the new page)
attr_writer :content_new_page
def content_new_page
@content_new_page || ''
end
# If provided, the minimum width that this cell in its column will permit.
#
def min_width_ignoring_span
......@@ -110,7 +117,9 @@ module Prawn
# Manually specify the cell's height.
#
attr_writer :height
attr_accessor :height
attr_accessor :original_height
# Specifies which borders to enable. Must be an array of zero or more of:
# <tt>[:left, :right, :top, :bottom]</tt>.
......@@ -155,6 +164,13 @@ module Prawn
#
attr_reader :dummy_cells
def filtered_dummy_cells(row_number = false, new_page = false)
@dummy_cells unless row_number
@dummy_cells.map do |dummy_cell|
dummy_cell if (dummy_cell.row <= row_number && !new_page) || (dummy_cell.row > row_number && new_page)
end.compact.uniq
end
# Instantiates a Cell based on the given options. The particular class of
# cell returned depends on the :content argument. See the Prawn::Table
# documentation under "Data" for allowable content types.
......@@ -220,6 +236,9 @@ module Prawn
@dummy_cells = []
options.each { |k, v| send("#{k}=", v) }
# save the height so we can get back to it later
@original_height = options[:height]
@initializer_run = true
end
......@@ -309,6 +328,22 @@ module Prawn
defined?(@height) && @height || (content_height + padding_top + padding_bottom)
end
def calculate_height_ignoring_span(respect_original_height = true)
# if a custom height was set, don't recalculate it
# if respect_original_height is set
return original_height if original_height && respect_original_height
natural_content_height + padding_top + padding_bottom
end
def recalculate_height_ignoring_span(respect_original_height = true)
@height = calculate_height_ignoring_span(respect_original_height)
@height
end
def height_of_cell
@height
end
# Returns the cell's height in points, inclusive of padding. If the cell
# is the master cell of a rowspan, returns the width of the entire span
# group.
......
......@@ -20,6 +20,9 @@ module Prawn
@padding = [0, 0, 0, 0]
end
# allow the corresponding master_cell to be read
attr_reader :master_cell
# By default, a span dummy will never increase the height demand.
#
def natural_content_height
......@@ -74,6 +77,11 @@ module Prawn
@master_cell.background_color
end
# is this a SpanDummy for a rowspan?
def row_dummy?
(row != @master_cell.row)
end
private
# Are we on the right border of the span?
......
......@@ -55,6 +55,7 @@ module Prawn
# preset width.
#
def natural_content_height
return 0 if content.nil?
with_font do
b = text_box(:width => spanned_content_width + FPTolerance)
b.render(:dry_run => true)
......
......@@ -175,6 +175,20 @@ module Prawn
aggregate_cell_values(:column, :max_width_ignoring_span, :max)
end
def recalculate_height
each do |cell|
cell.recalculate_height_ignoring_span
end
height
end
# reduce the y value in all cells by a given amount
def reduce_y(amount)
each do |cell|
cell.y -= amount
end
end
# Returns the total height of all rows in the selected set.
#
def height
......
# encoding: utf-8
module Prawn
class Table
# This class can do one thing well: split the content of a cell
# while doing this it also adjust the height of the cell to something reasonable
class SplitCell
def initialize(cell, cells=false)
@cell = cell
@cells = cells
if cell.content
@original_content = cell.content
@content_array = cell.content.split(' ')
end
end
attr_reader :cells
attr_accessor :cell
# split the content of the cell and adjust the height
def split(row_to_split, max_available_height)
# we don't process SpanDummy cells
return cell if cell.is_a?(Prawn::Table::Cell::SpanDummy)
return cell unless row_to_split == cell.row
# prepare everything for the while loop
# first we're gonna check if a single word fits the available space
i = 0
cell.content = @content_array[0]
content_that_fits = ''
while recalculate_height <= max_available_height
# content from last round
content_that_fits = content_old_page(i)
break if @content_array[i].nil?
i += 1
cell.content = content_old_page(i)
end
split_content(content_that_fits, i)
# recalcualte height for the cell in question
recalculate_height(include_dummy_cells: true)
return cell
end
# should we adjust the offset?
def adjust_offset?(final_cell_last_page)
return false unless move_cells_off_canvas?
(cell.y > final_cell_last_page.y)
end
# calculate extra offset
def extra_offset(max_cell_heights_cached, final_cell_last_page)
return 0 if adjust_offset?(final_cell_last_page)
return 0 unless move_cells_off_canvas?
return height_of_additional_already_printed_rows(cells.last_row, max_cell_heights_cached)
end
# should we move the cell off canvas?
def move_cells_off_canvas?
cells.new_page &&
!cell.is_a?(Prawn::Table::Cell::SpanDummy) &&
!cell.dummy_cells.empty? &&
cell.row < cells.last_row
end
# returns the height of any rows that have already been printed
def height_of_additional_already_printed_rows(last_row, max_cell_heights_cached)
((cell.row+1..last_row)).map{ |row_number| max_cell_heights_cached[row_number]}.inject(:+)
end
# return array to be written to cells_this_page
def print(offset, max_cell_height_cached, final_cell_last_page, min_y)
# we might have to adjust the offset
adjust_offset = extra_offset(max_cell_height_cached, final_cell_last_page)
# if the offset has to be adjusted, also correct the y position
cell.y = final_cell_last_page.y if adjust_offset?(final_cell_last_page)
y = cell.relative_y(offset - adjust_offset)
y = min_y if final_cell_last_page && min_y > 0 && cell.row <= final_cell_last_page.row && y < min_y
cell_for_page = [cell, [cell.relative_x, y]]
end
private
# recalculates the height of the cell and dummy cells if specified
def recalculate_height(options = {})
new_height = cell.recalculate_height_ignoring_span
return new_height unless options[:include_dummy_cells] == true
# if a height was set for this cell, use it if the text didn't have to be split
# cell.height = cell.original_height if cell.content == old_content && !cell.original_height.nil?
# and its dummy cells
cell.dummy_cells.each do |dummy_cell|
dummy_cell.recalculate_height_ignoring_span
end
return new_height
end
# splits the content
def split_content(content_that_fits, i)
# did anything fit at all?
if !content_that_fits || content_that_fits.length == 0
cell.content = @original_content
return
end
cell.content = content_that_fits
cell.content_new_page = calculate_content_new_page(cell, i)
end
# return the content for a new page, based on the existing content_new_page
# and the calculated position in the content array where the cell is now split
def calculate_content_new_page(cell, i)
content_new_page = cell.content_new_page
if !cell.content_new_page.nil? && !(content_new_page(i).nil? || content_new_page(i) == '')
content_new_page = ' ' + content_new_page
end
content_new_page(i) + (content_new_page || '' )
end
# returns the content that should be printed to the old page
def content_old_page(i)
@content_array[0..i].join(' ')
end
# returns the content that should be printed to the new page
def content_new_page(i)
@content_array[i..-1].join(' ')
end
end
end
end
\ No newline at end of file
# encoding: utf-8
module Prawn
class Table
# This class knows everything about splitting an array of cells
class SplitCells
def initialize(cells, options = {})
@cells = cells
@current_row_number = options[:current_row_number]
@new_page = options[:new_page]
@table = options[:table]
@cells_this_page_option = options[:cells_this_page]
compensate_offset_for_height = 0
end
# allow this class to access the rows of the table
def rows(row_spec)
table.rows(row_spec)
end
alias_method :row, :rows
attr_accessor :cells
# the table associated with this instance
attr_reader :table
attr_reader :new_page
# change content to the one needed for the new page
def adjust_content_for_new_page
cells.each do |cell|
cell = handle_cells_this_page(cell)
cell.content = cell.content_new_page
end
end
# cells_this_page and split_cells are formatted differently
# adjust accordingly for cells.each calls
def handle_cells_this_page(cell)
return cell[0] if @cells_this_page_option
return cell
end
# adjust the height of the cells
def adjust_height_of_cells(options = {})
cells_we_have_passed = []
@cells.each do |cell|
cell = handle_cells_this_page(cell)
next if options[:skip_rows] && options[:skip_rows].include?(cell.row)
# remember that we've passed this cell (for future dummy cells)
#
# ensure that the array is two dimensional
cells_we_have_passed[cell.row]=[] if cells_we_have_passed[cell.row].nil?
# remember
cells_we_have_passed[cell.row][cell.column] = true
# skip cell if it is a dummy cell not included in the set of cells we
# are looping through
if cell.is_a?(Prawn::Table::Cell::SpanDummy)
master_cell = cell.master_cell
next if cells_we_have_passed[master_cell.row].nil? || cells_we_have_passed[master_cell.row][master_cell.column].nil?
end
# height of the current cell
height_of_cell = max_row_height(cell) || 0
puts "cell #{cell.row}/#{cell.column} height=#{height_of_cell} (sc 69)"
if options[:min_row_height] && !cell.is_a?(Prawn::Table::Cell::SpanDummy) && cell.row != last_row
min_row_height = options[:min_row_height][cell.row] || 0
height_of_cell = min_row_height if min_row_height > height_of_cell
end
puts "cell #{cell.row}/#{cell.column} height=#{height_of_cell} (sc 75)"
puts "cell #{cell.row}/#{cell.column} min_row_height=#{options[:min_row_height]} (sc 76)"
puts "cell #{cell.row}/#{cell.column} extra_height_for_row_dummies=#{extra_height_for_row_dummies(cell)} (sc 77)"
row_dummies_height = extra_height_for_row_dummies(cell)
if !@new_page && options[:min_row_height]
#row_dummies_height = 93+30
min_height = 0
cell.filtered_dummy_cells(last_row, @new_page).each do |dummy_cell|
next unless dummy_cell.column == cell.column
if dummy_cell.row != last_row
puts "cell #{cell.row}/#{cell.column} adding dummy height for row #{dummy_cell.row} of #{options[:min_row_height][dummy_cell.row] || 0}"
min_height += options[:min_row_height][dummy_cell.row] || 0
else
min_height += row(dummy_cell.row).height
end
end
row_dummies_height = min_height if min_height > row_dummies_height
end
# account for other rows that this cell spans
cell.height = height_of_cell + row_dummies_height
end
end
# we don't want to resize header cells and cells from earlier pages
# (that span into the current one) on the final page
def adjust_height_of_final_cells(header_rows, first_row_new_page, options = {})
skip_row_numbers = []
# don't resize the header
header_rows.each do |cell|
skip_row_numbers.push cell.row
end
# don't resize cells from former pages (that span into this page)
0..first_row_new_page.times do |i|
skip_row_numbers.push i
end
skip_row_numbers.uniq!
cells.each do |cell, stuff|
puts "cell #{cell.row}/#{cell.column} height=#{cell.height_of_cell} (sc 116)"
end
options[:skip_rows] = skip_row_numbers
adjust_height_of_cells(options)
cells.each do |cell, stuff|
puts "cell #{cell.row}/#{cell.column} height=#{cell.height_of_cell} (sc 122)"
end
return cells
end
# calculate the maximum height of each row
def max_cell_heights(force_reload = false)
# cache the result
return @max_cell_heights if !force_reload && defined? @max_cell_heights
@max_cell_heights = Hash.new(0)
cells.each do |cell|
cell = handle_cells_this_page(cell)
next if cell.content.nil? || cell.content.empty?
# if we are on the new page, change the content of the cell
# cell.content = cell.content_new_page if hash[:new_page]
# calculate the height of the cell includign any cells it may span
respect_original_height = true unless @new_page
cell_height = cell.calculate_height_ignoring_span(respect_original_height)
# account for the height of any rows this cell spans (new page)
cell_height -= height_of_row_dummies(cell)
@max_cell_heights[cell.row] = cell_height if @max_cell_heights[cell.row] < cell_height
end
@max_cell_heights
end
attr_accessor :compensate_offset_for_height
# calculate by how much we have to compensate the offset
def compensate_offset
(max_cell_height.values.max || 0) - compensate_offset_for_height
end
# remove any cells from the cells array that are not needed on the new page
def cells_new_page
# is there some content to display coming from the last row on the last page?
# found_some_content_in_the_last_row_on_the_last_page = false
# cells.each do |split_cell|
# next unless split_cell.row == last_row_number_last_page
# found_some_content_in_the_last_row_on_the_last_page = true unless split_cell.content_new_page.nil? || split_cell.content_new_page.empty?
# end
cells_new_page = []
cells.each do |split_cell|
split_cell = handle_cells_this_page(split_cell)
next if irrelevant_cell?(split_cell)
# all tests passed. print it - meaning add it to the array
cells_new_page.push split_cell
end
cells_new_page
end
# return the cells to be used on the old page
def cells_old_page
# obviously we wouldn't have needed a function for this,
# but it makes the code more readable at the place
# that calls this function
cells
end
# the number of the first row
def first_row
return cells.first[0].row if @cells_this_page_option
cells.first.row
end
# the number of the last row
def last_row
return cells.last[0].row if @cells_this_page_option
cells.last.row
end
# resplit the content
# meaning that we ensure that the content really fits
# into the cell on the old page and resplit it if necessary
def resplit_content
cells.each do |cell|
cell.height = 0 unless cell.is_a?(Prawn::Table::Cell::SpanDummy)
max_available_height = rows(first_row..last_row).height
Prawn::Table::SplitCell.new(cell).split(cell.row, max_available_height)
end
return cells
end
def min_y
last_row_last_page = 0
if @new_page && !cells.empty?
min_y = nil
compensate_height = 0
cells.each do |c, stuff|
if min_y.nil? || min_y > stuff[1]
min_y = stuff[1]
compensate_height = c.height
end
end
end
return (min_y || 0) - (compensate_height || 0)
end
private
# cells that aren't located in the last row and that don't span
# the last row with an attached dummy cell are irrelevant
# for the splitting process
def irrelevant_cell?(cell)
# don't print cells that don't span anything and that
# aren't located in the last row
return true if cell.row < last_row_number_last_page &&
cell.dummy_cells.empty? &&