prawn-layout 0.1.0

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,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