prawn-layout 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,263 @@
1
+ # encoding: utf-8
2
+
3
+ # cell.rb : Table support functions
4
+ #
5
+ # Copyright June 2008, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ module Prawn
9
+
10
+ class Document
11
+ # Builds and renders a Table::Cell. A cell is essentially a
12
+ # special-purpose bounding box designed for flowing text within a bordered
13
+ # area. For available options, see Table::Cell#new.
14
+ #
15
+ # Prawn::Document.generate("cell.pdf") do
16
+ # cell [100,500],
17
+ # :width => 200,
18
+ # :text => "The rain in Spain falls mainly on the plains"
19
+ # end
20
+ #
21
+ def cell(point, options={})
22
+ Prawn::Table::Cell.new(
23
+ options.merge(:document => self, :point => point)).draw
24
+ end
25
+ end
26
+
27
+ class Table
28
+ # A cell is a special-purpose bounding box designed to flow text within a
29
+ # bordered area. This is used by Prawn's Document::Table implementation but
30
+ # can also be used standalone for drawing text boxes via Document#cell
31
+ #
32
+ class Cell
33
+
34
+ # Creates a new cell object. Generally used indirectly via Document#cell
35
+ #
36
+ # Of the available options listed below, <tt>:point</tt>, <tt>:width</tt>,
37
+ # and <tt>:text</tt> must be provided. If you are not using the
38
+ # Document#cell shortcut, the <tt>:document</tt> must also be provided.
39
+ #
40
+ # <tt>:point</tt>:: Absolute [x,y] coordinate of the top-left corner of the cell.
41
+ # <tt>:document</tt>:: The Prawn::Document object to render on.
42
+ # <tt>:text</tt>:: The text to be flowed within the cell
43
+ # <tt>:text_color</tt>:: The color of the text to be displayed
44
+ # <tt>:width</tt>:: The width in PDF points of the cell.
45
+ # <tt>:height</tt>:: The height in PDF points of the cell.
46
+ # <tt>:horizontal_padding</tt>:: The horizontal padding in PDF points
47
+ # <tt>:vertical_padding</tt>:: The vertical padding in PDF points
48
+ # <tt>:padding</tt>:: Overrides both horizontal and vertical padding
49
+ # <tt>:align</tt>:: One of <tt>:left</tt>, <tt>:right</tt>, <tt>:center</tt>
50
+ # <tt>:borders</tt>:: An array of sides which should have a border. Any of <tt>:top</tt>, <tt>:left</tt>, <tt>:right</tt>, <tt>:bottom</tt>
51
+ # <tt>:border_width</tt>:: The border line width. Defaults to 1.
52
+ # <tt>:border_style</tt>:: One of <tt>:all</tt>, <tt>:no_top</tt>, <tt>:no_bottom</tt>, <tt>:sides</tt>, <tt>:none</tt>, <tt>:bottom_only</tt>. Defaults to :all.
53
+ # <tt>:border_color</tt>:: The color of the cell border.
54
+ # <tt>:font_size</tt>:: The font size for the cell text.
55
+ #
56
+ def initialize(options={})
57
+ @point = options[:point]
58
+ @document = options[:document]
59
+ @text = options[:text].to_s
60
+ @text_color = options[:text_color]
61
+ @width = options[:width]
62
+ @height = options[:height]
63
+ @borders = options[:borders]
64
+ @border_width = options[:border_width] || 1
65
+ @border_style = options[:border_style] || :all
66
+ @border_color = options[:border_color]
67
+ @background_color = options[:background_color]
68
+ @align = options[:align] || :left
69
+ @font_size = options[:font_size]
70
+
71
+ @horizontal_padding = options[:horizontal_padding] || 0
72
+ @vertical_padding = options[:vertical_padding] || 0
73
+
74
+ if options[:padding]
75
+ @horizontal_padding = @vertical_padding = options[:padding]
76
+ end
77
+ end
78
+
79
+ attr_accessor :point, :border_style, :border_width, :background_color,
80
+ :document, :horizontal_padding, :vertical_padding, :align,
81
+ :borders, :text_color, :border_color
82
+
83
+ attr_writer :height, :width #:nodoc:
84
+
85
+ # Returns the cell's text as a string.
86
+ #
87
+ def to_s
88
+ @text
89
+ end
90
+
91
+ # The width of the text area excluding the horizonal padding
92
+ #
93
+ def text_area_width
94
+ width - 2*@horizontal_padding
95
+ end
96
+
97
+ # The width of the cell in PDF points
98
+ #
99
+ def width
100
+ @width || (@document.font.width_of(@text, :size => @font_size)) + 2*@horizontal_padding
101
+ end
102
+
103
+ # The height of the cell in PDF points
104
+ #
105
+ def height
106
+ @height || text_area_height + 2*@vertical_padding
107
+ end
108
+
109
+ # The height of the text area excluding the vertical padding
110
+ #
111
+ def text_area_height
112
+ @document.height_of(@text, text_area_width)
113
+ end
114
+
115
+ # Draws the cell onto the PDF document
116
+ #
117
+ def draw
118
+ rel_point = @point
119
+
120
+ if @background_color
121
+ @document.mask(:fill_color) do
122
+ @document.fill_color @background_color
123
+ h = borders.include?(:bottom) ?
124
+ height - border_width : height + border_width / 2.0
125
+ @document.fill_rectangle [rel_point[0] + border_width / 2.0,
126
+ rel_point[1] - border_width / 2.0 ],
127
+ width - border_width, h
128
+ end
129
+ end
130
+
131
+ if @border_width > 0
132
+ @document.mask(:line_width) do
133
+ @document.line_width = @border_width
134
+
135
+ @document.mask(:stroke_color) do
136
+ @document.stroke_color @border_color if @border_color
137
+
138
+ if borders.include?(:left)
139
+ @document.stroke_line [rel_point[0], rel_point[1] + (@border_width / 2.0)],
140
+ [rel_point[0], rel_point[1] - height - @border_width / 2.0 ]
141
+ end
142
+
143
+ if borders.include?(:right)
144
+ @document.stroke_line(
145
+ [rel_point[0] + width, rel_point[1] + (@border_width / 2.0)],
146
+ [rel_point[0] + width, rel_point[1] - height - @border_width / 2.0] )
147
+ end
148
+
149
+ if borders.include?(:top)
150
+ @document.stroke_line(
151
+ [ rel_point[0] + @border_width / 2.0, rel_point[1] ],
152
+ [ rel_point[0] - @border_width / 2.0 + width, rel_point[1] ])
153
+ end
154
+
155
+ if borders.include?(:bottom)
156
+ @document.stroke_line [rel_point[0], rel_point[1] - height ],
157
+ [rel_point[0] + width, rel_point[1] - height]
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ borders
164
+
165
+ end
166
+
167
+ @document.bounding_box( [@point[0] + @horizontal_padding,
168
+ @point[1] - @vertical_padding],
169
+ :width => text_area_width,
170
+ :height => height - @vertical_padding) do
171
+ @document.move_down((@document.font.line_gap - @document.font.descender)/2)
172
+
173
+ options = {:align => @align, :final_gap => false}
174
+
175
+ options[:size] = @font_size if @font_size
176
+
177
+ @document.mask(:fill_color) do
178
+ @document.fill_color @text_color if @text_color
179
+ @document.text @text, options
180
+ end
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def borders
187
+ @borders ||= case @border_style
188
+ when :all
189
+ [:top,:left,:right,:bottom]
190
+ when :sides
191
+ [:left,:right]
192
+ when :no_top
193
+ [:left,:right,:bottom]
194
+ when :no_bottom
195
+ [:left,:right,:top]
196
+ when :bottom_only
197
+ [:bottom]
198
+ when :none
199
+ []
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+ class CellBlock #:nodoc:
206
+
207
+ # Not sure if this class is something I want to expose in the public API.
208
+
209
+ def initialize(document)
210
+ @document = document
211
+ @cells = []
212
+ @width = 0
213
+ @height = 0
214
+ end
215
+
216
+ attr_reader :width, :height, :cells
217
+ attr_accessor :background_color, :text_color, :border_color
218
+
219
+ def <<(cell)
220
+ @cells << cell
221
+ @height = cell.height if cell.height > @height
222
+ @width += cell.width
223
+ self
224
+ end
225
+
226
+ def draw
227
+ y = @document.y
228
+ x = @document.bounds.absolute_left
229
+
230
+ @cells.each do |e|
231
+ e.point = [x - @document.bounds.absolute_left,
232
+ y - @document.bounds.absolute_bottom]
233
+ e.height = @height
234
+ e.background_color ||= @background_color
235
+ e.text_color ||= @text_color
236
+ e.border_color ||= @border_color
237
+ e.draw
238
+ x += e.width
239
+ end
240
+
241
+ @document.y = y - @height
242
+ end
243
+
244
+ def border_width
245
+ @cells[0].border_width
246
+ end
247
+
248
+ def border_style=(s)
249
+ @cells.each { |e| e.border_style = s }
250
+ end
251
+
252
+ def align=(align)
253
+ @cells.each { |e| e.align = align }
254
+ end
255
+
256
+ def border_style
257
+ @cells[0].border_style
258
+ end
259
+
260
+ end
261
+ end
262
+
263
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ puts "Prawn specs: Running on Ruby Version: #{RUBY_VERSION}"
4
+
5
+ require "rubygems"
6
+ require "test/spec"
7
+ require "mocha"
8
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
9
+
10
+ require "prawn"
11
+ require "prawn/layout"
12
+ $LOAD_PATH << File.join(Prawn::BASEDIR, 'vendor','pdf-inspector','lib')
13
+
14
+ Prawn.debug = true
15
+
16
+ gem 'pdf-reader', ">=0.7.3"
17
+ require "pdf/reader"
18
+ require "pdf/inspector"
19
+
20
+ def create_pdf(klass=Prawn::Document)
21
+ @pdf = klass.new(:left_margin => 0,
22
+ :right_margin => 0,
23
+ :top_margin => 0,
24
+ :bottom_margin => 0)
25
+ end
@@ -0,0 +1,261 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
+
5
+ describe "A table's width" do
6
+ it "should equal sum(column_widths)" do
7
+ pdf = Prawn::Document.new
8
+ table = Prawn::Table.new( [%w[ a b c ], %w[d e f]], pdf,
9
+ :column_widths => { 0 => 50, 1 => 100, 2 => 150 })
10
+
11
+ table.width.should == 300
12
+ end
13
+
14
+ it "should calculate unspecified column widths as "+
15
+ "(max(string_width).ceil + 2*horizontal_padding)" do
16
+ pdf = Prawn::Document.new
17
+ hpad, fs = 3, 12
18
+ columns = 2
19
+ table = Prawn::Table.new( [%w[ foo b ], %w[d foobar]], pdf,
20
+ :horizontal_padding => hpad, :font_size => fs)
21
+
22
+ col0_width = pdf.font.width_of("foo", :size => fs)
23
+ col1_width = pdf.font.width_of("foobar", :size => fs)
24
+
25
+ table.width.should == col0_width.ceil + col1_width.ceil + 2*columns*hpad
26
+ end
27
+
28
+ it "should allow mixing autocalculated and preset"+
29
+ "column widths within a single table" do
30
+
31
+ pdf = Prawn::Document.new
32
+ hpad, fs = 10, 6
33
+ stretchy_columns = 2
34
+
35
+ col0_width = 50
36
+ col1_width = pdf.font.width_of("foo", :size => fs)
37
+ col2_width = pdf.font.width_of("foobar", :size => fs)
38
+ col3_width = 150
39
+
40
+ table = Prawn::Table.new( [%w[snake foo b apple],
41
+ %w[kitten d foobar banana]], pdf,
42
+ :horizontal_padding => hpad, :font_size => fs,
43
+ :column_widths => { 0 => col0_width, 3 => col3_width } )
44
+
45
+ table.width.should == col1_width.ceil + col2_width.ceil +
46
+ 2*stretchy_columns*hpad +
47
+ col0_width.ceil + col3_width.ceil
48
+
49
+ end
50
+
51
+ it "should not exceed the maximum width of the margin_box" do
52
+
53
+ pdf = Prawn::Document.new
54
+ expected_width = pdf.margin_box.width
55
+
56
+ data = [
57
+ ['This is a column with a lot of text that should comfortably exceed the width of a normal document margin_box width', 'Some more text', 'and then some more', 'Just a bit more to be extra sure']
58
+ ]
59
+
60
+ table = Prawn::Table.new(data, pdf)
61
+
62
+ table.width.should == expected_width
63
+
64
+ end
65
+
66
+ it "should not exceed the maximum width of the margin_box even with manual widths specified" do
67
+
68
+ pdf = Prawn::Document.new
69
+ expected_width = pdf.margin_box.width
70
+
71
+ data = [
72
+ ['This is a column with a lot of text that should comfortably exceed the width of a normal document margin_box width', 'Some more text', 'and then some more', 'Just a bit more to be extra sure']
73
+ ]
74
+
75
+ table = Prawn::Table.new(data, pdf, :column_widths => { 1 => 100 })
76
+
77
+ table.width.should == expected_width
78
+
79
+ end
80
+
81
+ it "should be the width of the :width parameter" do
82
+
83
+ pdf = Prawn::Document.new
84
+ expected_width = 300
85
+
86
+ table = Prawn::Table.new( [%w[snake foo b apple],
87
+ %w[kitten d foobar banana]], pdf,
88
+ :width => expected_width
89
+ )
90
+
91
+ table.width.should == expected_width
92
+
93
+ end
94
+
95
+ it "should not exceed the :width option" do
96
+
97
+ pdf = Prawn::Document.new
98
+ expected_width = 400
99
+
100
+ data = [
101
+ ['This is a column with a lot of text that should comfortably exceed the width of a normal document margin_box width', 'Some more text', 'and then some more', 'Just a bit more to be extra sure']
102
+ ]
103
+
104
+ table = Prawn::Table.new(data, pdf, :width => expected_width)
105
+
106
+ table.width.should == expected_width
107
+
108
+ end
109
+
110
+ it "should not exceed the :width option even with manual widths specified" do
111
+
112
+ pdf = Prawn::Document.new
113
+ expected_width = 400
114
+
115
+ data = [
116
+ ['This is a column with a lot of text that should comfortably exceed the width of a normal document margin_box width', 'Some more text', 'and then some more', 'Just a bit more to be extra sure']
117
+ ]
118
+
119
+ table = Prawn::Table.new(data, pdf, :column_widths => { 1 => 100 }, :width => expected_width)
120
+
121
+ table.width.should == expected_width
122
+
123
+ end
124
+
125
+ end
126
+
127
+ describe "A table's height" do
128
+
129
+ before :each do
130
+ data = [["foo"],["bar"],["baaaz"]]
131
+ pdf = Prawn::Document.new
132
+ @num_rows = data.length
133
+
134
+ @vpad = 4
135
+ origin = pdf.y
136
+ pdf.table data, :vertical_padding => @vpad
137
+
138
+ @table_height = origin - pdf.y
139
+
140
+ @font_height = pdf.font.height
141
+ end
142
+
143
+ it "should have a height of n rows" do
144
+ @table_height.should.be.close(
145
+ @num_rows*@font_height + 2*@vpad*@num_rows, 0.001 )
146
+ end
147
+
148
+ end
149
+
150
+ describe "A table's content" do
151
+
152
+ it "should not cause an error if rendering the very first row causes a page break" do
153
+ Prawn::Document.new( :page_layout => :portrait ) do
154
+ arr = Array(1..5).collect{|i| ["cell #{i}"] }
155
+
156
+ move_down( y - (bounds.absolute_bottom + 3) )
157
+
158
+ lambda {
159
+ table( arr,
160
+ :font_size => 9,
161
+ :horizontal_padding => 3,
162
+ :vertical_padding => 3,
163
+ :border_width => 0.05,
164
+ :border_style => :none,
165
+ :row_colors => %w{ffffff eeeeee},
166
+ :column_widths => {0 =>110},
167
+ :position => :left,
168
+ :headers => ["exploding header"],
169
+ :align => :left,
170
+ :align_headers => :center)
171
+ }.should.not.raise
172
+ end
173
+ end
174
+
175
+ it "should output content cell by cell, row by row" do
176
+ data = [["foo","bar"],["baz","bang"]]
177
+ @pdf = Prawn::Document.new
178
+ @pdf.table(data)
179
+ output = PDF::Inspector::Text.analyze(@pdf.render)
180
+ output.strings.should == data.flatten
181
+ end
182
+
183
+ it "should add headers to output when specified" do
184
+ data = [["foo","bar"],["baz","bang"]]
185
+ headers = %w[a b]
186
+ @pdf = Prawn::Document.new
187
+ @pdf.table(data, :headers => headers)
188
+ output = PDF::Inspector::Text.analyze(@pdf.render)
189
+ output.strings.should == headers + data.flatten
190
+ end
191
+
192
+ it "should repeat headers across pages" do
193
+ data = [["foo","bar"]]*30
194
+ headers = ["baz","foobar"]
195
+ @pdf = Prawn::Document.new
196
+ @pdf.table(data, :headers => headers)
197
+ output = PDF::Inspector::Text.analyze(@pdf.render)
198
+ output.strings.should == headers + data.flatten[0..-3] + headers +
199
+ data.flatten[-2..-1]
200
+ end
201
+
202
+ it "should allow empty fields" do
203
+ lambda {
204
+ data = [["foo","bar"],["baz",""]]
205
+ @pdf = Prawn::Document.new
206
+ @pdf.table(data)
207
+ }.should.not.raise
208
+ end
209
+
210
+ it "should paginate for large tables" do
211
+ # 30 rows fit on the table with default setting, 31 exceed.
212
+ data = [["foo"]] * 31
213
+ pdf = Prawn::Document.new
214
+
215
+ pdf.table data
216
+ pdf.page_count.should == 2
217
+
218
+ pdf.table data
219
+ pdf.page_count.should == 3
220
+ end
221
+
222
+ it "should accurately count columns from data" do
223
+ # First data row may contain colspan which would hide true column count
224
+ data = [["Name:",{:text => "Some very long name", :colspan => 5}]]
225
+ pdf = Prawn::Document.new
226
+ table = Prawn::Table.new data, pdf
227
+ table.column_widths.length.should == 6
228
+ end
229
+
230
+ end
231
+
232
+ describe "An invalid table" do
233
+
234
+ before(:each) do
235
+ @pdf = Prawn::Document.new
236
+ @bad_data = ["Single Nested Array"]
237
+ end
238
+
239
+ it "should raise error when invalid table data is given" do
240
+ assert_raises(Prawn::Errors::InvalidTableData) do
241
+ @pdf.table(@bad_data)
242
+ end
243
+ end
244
+
245
+ it "should raise an EmptyTableError with empty table data" do
246
+ lambda {
247
+ data = []
248
+ @pdf = Prawn::Document.new
249
+ @pdf.table(data)
250
+ }.should.raise( Prawn::Errors::EmptyTable )
251
+ end
252
+
253
+ it "should raise an EmptyTableError with nil table data" do
254
+ lambda {
255
+ data = nil
256
+ @pdf = Prawn::Document.new
257
+ @pdf.table(data)
258
+ }.should.raise( Prawn::Errors::EmptyTable )
259
+ end
260
+
261
+ end