Commit f1855076 authored by Adam Gustafson's avatar Adam Gustafson
Browse files
parents 515f2db2 759a27b6
Pipeline #280 failed with stages
Gemfile.lock
manual.pdf
rotate.pdf
......@@ -16,6 +16,9 @@ require_relative 'table/cell/text'
require_relative 'table/cell/subtable'
require_relative 'table/cell/image'
require_relative 'table/cell/span_dummy'
require_relative 'table/cell/formatted/wrap'
require_relative 'table/cell/formatted/box'
require_relative 'table/cell/box'
module Prawn
module Errors
......
# encoding: utf-8
# box.rb : Implements table cell boxes
#
# Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
#
module Prawn
class Table
class Cell
# Generally, one would use the Prawn::Table#new method to create a table
#
class Box < Prawn::Table::Cell::Formatted::Box
def initialize(string, options={})
super([{ :text => string }], options)
end
def render(flags={})
leftover = super(flags)
leftover.collect { |hash| hash[:text] }.join
end
end
end
end
end
\ No newline at end of file
# encoding: utf-8
# formatted/box.rb : Implements formatted table box
#
# Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
#
module Prawn
class Table
class Cell
module Formatted
# @group Stable API
# Generally, one would use the Prawn::Text::Formatted#formatted_text_box
# convenience method. However, using Table::Cell::Formatted::Box.new lets
# you create a formatted box with the table cell rotation algorithm. In
# conjunction with #render(:dry_run => true) you can do look-ahead
# calculations prior to placing text on the page, or to determine how much
# vertical space was consumed by the printed text
#
class Box < Prawn::Text::Formatted::Box
include Prawn::Table::Cell::Formatted::Wrap
@x_correction = @y_correction = nil
def initialize(formatted_text, options={})
super formatted_text, options
# limit rotation to 0-90 until developed
@rotate = 90 if @rotate > 90
@rotate = 0 if @rotate < 0
end
def initialize_wrap(array)
super array
if @baseline_y == 0 && @rotate != 0 && @rotate != 90
# adjust vertical positioning so the first word fits
first_token = @line_wrap.tokenize(@arranger.preview_next_string).first
first_width = @document.width_of first_token
# find out how far down and left the first token
# must be moved so its top edge fits in the corner
hyp = -first_width * rotate_sin
@baseline_y = hyp * rotate_cos - @font_size
@x_correction = hyp * rotate_sin
@y_correction = -@baseline_y
# correct the height running over the bottom padding
# why is this necessary?
@height -= 5
end
end
# The width available at this point in the box
#
def available_width(baseline_y = @baseline_y)
if @rotate == 0
@width
elsif @rotate == 90
@height
else
baseline_top = -baseline_y - @font_size
# if the angle is smaller than the diagonal
if @height > aspect_height
if baseline_top < @width * rotate_sin
# in the top 'corner'
baseline_top / (rotate_sin * rotate_cos)
elsif baseline_top < @height * rotate_cos
# in the middle section
@width / rotate_cos - @line_height * rotate_tan
else
# the bottom 'corner'
(@height * rotate_cos + @width * rotate_sin + baseline_y) /
(rotate_cos * rotate_sin)
end
else # angle is larger than the diagonal
if baseline_top < @height * rotate_cos
# in the top 'corner'
baseline_top * rotate_sin_inv * rotate_cos_inv
elsif baseline_top < @width * rotate_sin
# in the middle section
@height * rotate_sin_inv - @line_height * rotate_tan_inv
else
# the bottom 'corner'
(@height * rotate_cos + @width * rotate_sin + baseline_y) *
(rotate_cos_inv * rotate_sin_inv)
end
end
end
end
# The height actually used during the previous <tt>render</tt>
#
def height(baseline_y = @baseline_y)
return 0 if baseline_y.nil? || @descender.nil?
if @rotate == 0 || @rotate == 90
(baseline_y - @descender).abs
else
((baseline_y - @descender).abs - @width/2) * rotate_cos_inv
end
end
# The height available at this point in the box
#
def available_height(width = @width)
if @rotate == 0
@height
elsif @rotate == 90
@width
else # outside corner to outside corner
@height * rotate_cos + @width * rotate_sin
end
end
# <tt>fragment</tt> is a Prawn::Text::Formatted::Fragment object
#
def draw_fragment(fragment, accumulated_width=0, line_width=0, word_spacing=0) #:nodoc:
last_baseline = @baseline_y+@line_height
case(@align)
when :left
x = @at[0]
when :center
x = @at[0] + (available_width(last_baseline) - line_width) * 0.5
when :right
x = @at[0] + available_width(last_baseline) - line_width
when :justify
if @direction == :ltr
x = @at[0]
else
x = @at[0] + available_width(last_baseline) - line_width
end
end
# @document.circle @at, 3
# bottom_left = [@at[0]-@height*rotate_sin,@at[1]-@height*rotate_cos]
# top_right = [@at[0]+@width*rotate_cos,@at[1]-@width*rotate_sin]
# @document.circle bottom_left, 3
# @document.circle top_right, 3
# @document.circle [bottom_left[0]+top_right[0], bottom_left[1]-(@at[1]-top_right[1])], 3
x += accumulated_width
y = @at[1] + @baseline_y + fragment.y_offset
# starting spot
# @document.circle [x,y+@y_correction], 1
# text location
# @document.circle [x+last_baseline*rotate_tan+@font_size*rotate_tan,y+@y_correction], 2
# uncorrected rectangle edge:
# @document.circle [x+last_baseline*rotate_tan,y+@y_correction], 2
if @rotate != 0 && @rotate != 90
height_actual = -last_baseline * rotate_cos_inv
# @document.circle [x+last_baseline*rotate_tan+(height_actual-@height)*rotate_sin_inv,y+@y_correction], 2
y += @y_correction
# we have reached the bottom corner of the cell
if height_actual > @height
x += last_baseline*rotate_tan+(height_actual-@height)*rotate_sin_inv
# check if the line overlaps the left side
test_y = Math.tan((90-@rotate)* Math::PI / 180)*(x-@at[0]) + @at[1]
if (y+@font_size) > test_y
x += (y+@font_size-test_y)*rotate_tan
end
else # move left
x += (last_baseline + @y_correction) * rotate_tan + @x_correction
end
end
fragment.left = x
fragment.baseline = y
if @inked
draw_fragment_underlays(fragment)
@document.word_spacing(word_spacing) {
if @draw_text_callback
@draw_text_callback.call(fragment.text, :at => [x, y],
:kerning => @kerning)
else
@document.draw_text!(fragment.text, :at => [x, y],
:kerning => @kerning)
end
}
draw_fragment_overlays(fragment)
end
end
def valid_options
PDF::Core::Text::VALID_OPTIONS + [:at, :height, :width,
:align, :valign,
:rotate,
:overflow, :min_font_size,
:leading, :character_spacing,
:mode, :single_line,
:skip_encoding,
:document,
:direction,
:fallback_fonts,
:draw_text_callback]
end
private
def render_rotated(text)
unprinted_text = ''
if @rotate == 90
x = @at[0] + @height/2.0 - 1.0
y = @at[1] - @height/2.0 + 4.0
else
x = @at[0]
y = @at[1]
end
@document.rotate(@rotate, :origin => [x, y]) do
unprinted_text = wrap(text)
end
unprinted_text
end
private
def aspect_height
@aspect_height ||= @width * rotate_tan
end
def rotate_complement_rads
@rotate_complement_rads ||= (90 - @rotate) * Math::PI / 180
end
def rotate_rads
@rotate_rads ||= @rotate * Math::PI / 180
end
def rotate_atan
Math.atan(@height/@width) * 180 / Math::PI
end
def rotate_tan
@rotate_tan ||= Math.tan(rotate_rads)
end
def rotate_tan_inv
@rotate_tan_inv ||= 1/rotate_tan
end
def rotate_cos
@rotate_cos ||= Math.cos(rotate_rads)
end
def rotate_cos_inv
@rotate_cos_inv ||= 1/rotate_cos
end
def rotate_sin
@rotate_sin ||= Math.sin(rotate_rads)
end
def rotate_sin_inv
@rotate_sin_inv ||= 1/rotate_sin
end
end
end
end
end
end
# encoding: utf-8
# formatted/wrap.rb : Implements formatted table box
#
# Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
#
module Prawn
class Table
class Cell
module Formatted
# @group Stable API
module Wrap
include Prawn::Text::Formatted::Wrap #:nodoc:
# See the developer documentation for PDF::Core::Text#wrap
#
# Formatted#wrap should set the following variables:
# <tt>@line_height</tt>::
# the height of the tallest fragment in the last printed line
# <tt>@descender</tt>::
# the descender height of the tallest fragment in the last
# printed line
# <tt>@ascender</tt>::
# the ascender heigth of the tallest fragment in the last
# printed line
# <tt>@baseline_y</tt>::
# the baseline of the current line
# <tt>@nothing_printed</tt>::
# set to true until something is printed, then false
# <tt>@everything_printed</tt>::
# set to false until everything printed, then true
#
# Returns any formatted text that was not printed
#
def wrap(array) #:nodoc:
initialize_wrap(array)
stop = false
while !stop
# wrap before testing if enough height for this line because the
# height of the highest fragment on this line will be used to
# determine the line height
begin
cannot_fit = false
@line_wrap.wrap_line(:document => @document,
:kerning => @kerning,
:width => available_width,
:arranger => @arranger,
:rotate => @rotate)
rescue Prawn::Errors::CannotFit
cannot_fit = true
end
if enough_height_for_this_line?
move_baseline_down
print_line unless cannot_fit
elsif cannot_fit
raise Prawn::Errors::CannotFit
else
stop = true
end
stop ||= @single_line || @arranger.finished?
end
@text = @printed_lines.join("\n")
@everything_printed = @arranger.finished?
@arranger.unconsumed
end
end
end
end
end
end
......@@ -123,10 +123,10 @@ module Prawn
options[:document] = @pdf
array = @pdf.text_formatter.format(@content, *p)
::Prawn::Text::Formatted::Box.new(array,
::Prawn::Table::Cell::Formatted::Box.new(array,
options.merge(extra_options).merge(:document => @pdf))
else
::Prawn::Text::Box.new(@content, @text_options.merge(extra_options).
::Prawn::Table::Cell::Box.new(@content, @text_options.merge(extra_options).
merge(:document => @pdf))
end
end
......
......@@ -463,10 +463,12 @@ describe "Prawn::Table::Cell" do
it "should pass through text options like :align to Text::Box" do
c = cell(:content => "text", :align => :right)
box = Prawn::Text::Box.new("text", :document => @pdf)
box = Prawn::Table::Cell::Box.new("text", :document => @pdf)
expect(Prawn::Text::Box).to receive(:new).with("text", hash_including(align: :right))
.at_least(:once).and_return(box)
Prawn::Table::Cell::Box.expects(:new).checking do |text, options|
text.should == "text"
options[:align].should == :right
end.at_least_once.returns(box)
c.draw
end
......@@ -474,10 +476,12 @@ describe "Prawn::Table::Cell" do
it "should use font_style for Text::Box#style" do
c = cell(:content => "text", :font_style => :bold)
box = Prawn::Text::Box.new("text", :document => @pdf)
box = Prawn::Table::Cell::Box.new("text", :document => @pdf)
expect(Prawn::Text::Box).to receive(:new).with("text", hash_including(style: :bold))
.at_least(:once).and_return(box)
Prawn::Table::Cell::Box.expects(:new).checking do |text, options|
text.should == "text"
options[:style].should == :bold
end.at_least_once.returns(box)
c.draw
end
......@@ -487,15 +491,12 @@ describe "Prawn::Table::Cell" do
c = cell(:content => "text", :font_style => :bold)
box = Prawn::Text::Box.new("text", :document => @pdf)
expect(Prawn::Text::Box).to receive(:new)
.and_wrap_original do |original_method, *args, &block|
text, options, = args
expect(text).to eq "text"
expect(options[:style]).to eq :bold
expect(@pdf.font.family).to eq 'Courier'
box
end.at_least(:once)
box = Prawn::Table::Cell::Box.new("text", :document => @pdf)
Prawn::Table::Cell::Box.expects(:new).checking do |text, options|
text.should == "text"
options[:style].should == :bold
@pdf.font.family.should == 'Courier'
end.at_least_once.returns(box)
c.draw
end
......@@ -506,15 +507,12 @@ describe "Prawn::Table::Cell" do
c = cell(:content => "text")
box = Prawn::Text::Box.new("text", :document => @pdf)
expect(Prawn::Text::Box).to receive(:new)
.and_wrap_original do |original_method, *args, &block|
text = args.first
expect(text).to eq "text"
expect(@pdf.font.family).to eq 'Courier'
expect(@pdf.font.options[:style]).to eq :bold
box
end.at_least(:once)
box = Prawn::Table::Cell::Box.new("text", :document => @pdf)
Prawn::Table::Cell::Box.expects(:new).checking do |text, options|
text.should == "text"
@pdf.font.family.should == 'Courier'
@pdf.font.options[:style].should == :bold
end.at_least_once.returns(box)
c.draw
end
......@@ -522,16 +520,18 @@ describe "Prawn::Table::Cell" do
it "should allow inline formatting in cells" do
c = cell(:content => "foo <b>bar</b> baz", :inline_format => true)
box = Prawn::Text::Formatted::Box.new([], :document => @pdf)
box = Prawn::Table::Cell::Formatted::Box.new([], :document => @pdf)
Prawn::Table::Cell::Formatted::Box.expects(:new).checking do |array, options|
array[0][:text].should == "foo "
array[0][:styles].should == []
array[1][:text].should == "bar"
array[1][:styles].should == [:bold]
expect(Prawn::Text::Formatted::Box).to receive(:new).with(
[
hash_including(text: "foo ", styles: []),
hash_including(text: "bar", styles: [:bold]),
hash_including(text: " baz", styles: [])
],
kind_of(Hash)
).at_least(:once).and_return(box)
array[2][:text].should == " baz"
array[2][:styles].should == []
end.at_least_once.returns(box)
c.draw
end
......
# encoding: utf-8
require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
describe "Table::Cell::Box#render with :rotate option)" do
before(:each) do
create_pdf
@lorem = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium."
end
it "should rotate table cell content" do
table = nil
@pdf.bounding_box([20,@pdf.bounds.height-100], :width => @pdf.bounds.width-40, :height => @pdf.bounds.height-20) do
# lorem = "| "*300
data = [
[
{content: "01 #{@lorem}", rotate: -5}, #coerced back to zero
{content: "02 #{@lorem}", rotate: 15},
{content: "03 #{@lorem}", rotate: 30},
{content: "04 #{@lorem}", rotate: 45},
{content: "05 #{@lorem}", rotate: 60},
{content: "06 #{@lorem}", rotate: 75},
{content: "07 #{@lorem}", rotate: 95}, #coerced back to 90
],
]
column_widths = {}
table = @pdf.table data, :header => false, :row_colors => ["EEEEEE", "FFFFFF"], :width => @pdf.bounds.width, :cell_style => {:padding => 3, :size => 8, :align => :left} do |t|
t.column(1).align = :center
t.column(2).align = :right
t.column(4).align = :right
end
end
matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
# matrices.matrices.should == [[1.0, 0.0, 0.0, 1.0, 181.2142, -3.71449], [0.96593, 0.25882, -0.25882, 0.96593, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0, 368.16269, -1.25787], [0.86603, 0.5, -0.5, 0.86603, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0, 563.87552, 11.42807], [0.70711, 0.70711, -0.70711, 0.70711, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0, 769.34416, 40.20083], [0.5, 0.86603, -0.86603, 0.5, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0, 982.85696, 91.85987], [0.25882, 0.96593, -0.96593, 0.25882, 0.0, 0.0], [1.0, 0.0, 0.0, 1.0, 1202.65771, -115.5087], [0.0, 1.0, -1.0, 0.0, 0.0, 0.0]]
matrices.matrices[0].should == [1.0, 0.0, 0.0, 1.0, 181.2142, -3.71449]
matrices.matrices[2].should == [1.0, 0.0, 0.0, 1.0, 368.16269, -1.25787]
matrices.matrices[4].should == [1.0, 0.0, 0.0, 1.0, 563.87552, 11.42807]
[15,30,45].each_with_index do |rotate, i|
cos = reduce_precision(Math.cos(rotate * Math::PI / 180))
sin = reduce_precision(Math.sin(rotate * Math::PI / 180))
matrices.matrices[i*2+1].should == [cos, sin, -sin, cos, 0, 0]
end
text = PDF::Inspector::Text.analyze(@pdf.render)
text.strings.length.should == 134
# @pdf.render_file "rotate.pdf"
end
end
def reduce_precision(float)
("%.5f" % float).to_f
end
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment