sskirby-prawn-layout 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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