sskirby-prawn-layout 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,274 @@
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
+ # <tt>:font_style</tt>:: The font style for the cell text.
56
+ #
57
+ def initialize(options={})
58
+ @point = options[:point]
59
+ @document = options[:document]
60
+ @text = options[:text].to_s
61
+ @text_color = options[:text_color]
62
+ @width = options[:width]
63
+ @height = options[:height]
64
+ @borders = options[:borders]
65
+ @border_width = options[:border_width] || 1
66
+ @border_style = options[:border_style] || :all
67
+ @border_color = options[:border_color]
68
+ @background_color = options[:background_color]
69
+ @align = options[:align] || :left
70
+ @font_size = options[:font_size]
71
+ @font_style = options[:font_style]
72
+
73
+ @horizontal_padding = options[:horizontal_padding] || 0
74
+ @vertical_padding = options[:vertical_padding] || 0
75
+
76
+ if options[:padding]
77
+ @horizontal_padding = @vertical_padding = options[:padding]
78
+ end
79
+ end
80
+
81
+ attr_accessor :point, :border_style, :border_width, :background_color,
82
+ :document, :horizontal_padding, :vertical_padding, :align,
83
+ :borders, :text_color, :border_color, :font_size, :font_style
84
+
85
+ attr_writer :height, :width #:nodoc:
86
+
87
+ # Returns the cell's text as a string.
88
+ #
89
+ def to_s
90
+ @text
91
+ end
92
+
93
+ # The width of the text area excluding the horizonal padding
94
+ #
95
+ def text_area_width
96
+ width - 2*@horizontal_padding
97
+ end
98
+
99
+ # The width of the cell in PDF points
100
+ #
101
+ def width
102
+ @width || (@document.width_of(@text, :size => @font_size)) + 2*@horizontal_padding
103
+ end
104
+
105
+ # The height of the cell in PDF points
106
+ #
107
+ def height
108
+ @height || text_area_height + 2*@vertical_padding
109
+ end
110
+
111
+ # The height of the text area excluding the vertical padding
112
+ #
113
+ def text_area_height
114
+ text_height = 0
115
+ if @font_size
116
+ @document.font_size(@font_size) do
117
+ text_height = @document.height_of(@text, text_area_width)
118
+ end
119
+ else
120
+ text_height = @document.height_of(@text, text_area_width)
121
+ end
122
+ text_height
123
+ end
124
+
125
+ # Draws the cell onto the PDF document
126
+ #
127
+ def draw
128
+ rel_point = @point
129
+
130
+ if @background_color
131
+ @document.mask(:fill_color) do
132
+ @document.fill_color @background_color
133
+ h = borders.include?(:bottom) ?
134
+ height - border_width : height + border_width / 2.0
135
+ @document.fill_rectangle [rel_point[0] + border_width / 2.0,
136
+ rel_point[1] - border_width / 2.0 ],
137
+ width - border_width, h
138
+ end
139
+ end
140
+
141
+ if @border_width > 0
142
+ @document.mask(:line_width) do
143
+ @document.line_width = @border_width
144
+
145
+ @document.mask(:stroke_color) do
146
+ @document.stroke_color @border_color if @border_color
147
+
148
+ if borders.include?(:left)
149
+ @document.stroke_line [rel_point[0], rel_point[1] + (@border_width / 2.0)],
150
+ [rel_point[0], rel_point[1] - height - @border_width / 2.0 ]
151
+ end
152
+
153
+ if borders.include?(:right)
154
+ @document.stroke_line(
155
+ [rel_point[0] + width, rel_point[1] + (@border_width / 2.0)],
156
+ [rel_point[0] + width, rel_point[1] - height - @border_width / 2.0] )
157
+ end
158
+
159
+ if borders.include?(:top)
160
+ @document.stroke_line(
161
+ [ rel_point[0] + @border_width / 2.0, rel_point[1] ],
162
+ [ rel_point[0] - @border_width / 2.0 + width, rel_point[1] ])
163
+ end
164
+
165
+ if borders.include?(:bottom)
166
+ @document.stroke_line [rel_point[0], rel_point[1] - height ],
167
+ [rel_point[0] + width, rel_point[1] - height]
168
+ end
169
+ end
170
+
171
+ end
172
+
173
+ borders
174
+
175
+ end
176
+
177
+ @document.bounding_box( [@point[0] + @horizontal_padding,
178
+ @point[1] - @vertical_padding],
179
+ :width => text_area_width,
180
+ :height => height - @vertical_padding) do
181
+ @document.move_down((@document.font.line_gap - @document.font.descender)/2)
182
+
183
+ options = {:align => @align, :final_gap => false}
184
+
185
+ options[:size] = @font_size if @font_size
186
+ options[:style] = @font_style if @font_style
187
+
188
+ @document.mask(:fill_color) do
189
+ @document.fill_color @text_color if @text_color
190
+ @document.text @text, options
191
+ end
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def borders
198
+ @borders ||= case @border_style
199
+ when :all
200
+ [:top,:left,:right,:bottom]
201
+ when :sides
202
+ [:left,:right]
203
+ when :no_top
204
+ [:left,:right,:bottom]
205
+ when :no_bottom
206
+ [:left,:right,:top]
207
+ when :bottom_only
208
+ [:bottom]
209
+ when :none
210
+ []
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ class CellBlock #:nodoc:
217
+
218
+ # Not sure if this class is something I want to expose in the public API.
219
+
220
+ def initialize(document)
221
+ @document = document
222
+ @cells = []
223
+ @width = 0
224
+ @height = 0
225
+ end
226
+
227
+ attr_reader :width, :height, :cells
228
+ attr_accessor :background_color, :text_color, :border_color
229
+
230
+ def <<(cell)
231
+ @cells << cell
232
+ @height = cell.height if cell.height > @height
233
+ @width += cell.width
234
+ self
235
+ end
236
+
237
+ def draw
238
+ y = @document.y
239
+ x = @document.bounds.absolute_left
240
+
241
+ @cells.each do |e|
242
+ e.point = [x - @document.bounds.absolute_left,
243
+ y - @document.bounds.absolute_bottom]
244
+ e.height = @height
245
+ e.background_color ||= @background_color
246
+ e.text_color ||= @text_color
247
+ e.border_color ||= @border_color
248
+ e.draw
249
+ x += e.width
250
+ end
251
+
252
+ @document.y = y - @height
253
+ end
254
+
255
+ def border_width
256
+ @cells[0].border_width
257
+ end
258
+
259
+ def border_style=(s)
260
+ @cells.each { |e| e.border_style = s }
261
+ end
262
+
263
+ def align=(align)
264
+ @cells.each { |e| e.align = align }
265
+ end
266
+
267
+ def border_style
268
+ @cells[0].border_style
269
+ end
270
+
271
+ end
272
+ end
273
+
274
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
3
+
4
+ describe "When beginning each new page" do
5
+
6
+ it "should execute codeblock given to Document#header" do
7
+ call_count = 0
8
+
9
+ pdf = Prawn::Document.new
10
+ pdf.header(pdf.margin_box.top_left) do
11
+ call_count += 1
12
+ end
13
+
14
+ pdf.start_new_page
15
+ pdf.start_new_page
16
+ pdf.render
17
+
18
+ call_count.should == 3
19
+ end
20
+
21
+ end
22
+
23
+ describe "When ending each page" do
24
+
25
+ it "should execute codeblock given to Document#footer" do
26
+
27
+ call_count = 0
28
+
29
+ pdf = Prawn::Document.new
30
+ pdf.footer([pdf.margin_box.left, pdf.margin_box.bottom + 50]) do
31
+ call_count += 1
32
+ end
33
+
34
+ pdf.start_new_page
35
+ pdf.start_new_page
36
+ pdf.render
37
+
38
+ call_count.should == 3
39
+ end
40
+
41
+ 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.width_of("foo", :size => fs)
23
+ col1_width = pdf.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.width_of("foo", :size => fs)
37
+ col2_width = pdf.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