jyurek-prawn-layout 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ # layout/page.rb : Provides helpers for page layout
4
+ #
5
+ # Copyright January 2009, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Document
11
+ # A LazyBoundingBox is simply a BoundingBox with an action tied to it to be
12
+ # executed later. The lazy_bounding_box method takes the same arguments as
13
+ # bounding_box, but returns a LazyBoundingBox object instead of executing
14
+ # the code block directly.
15
+ #
16
+ # You can then call LazyBoundingBox#draw at any time (or multiple times if
17
+ # you wish), and the contents of the block will then be run. This can be
18
+ # useful for assembling repeating page elements or reusable components.
19
+ #
20
+ # file = "lazy_bounding_boxes.pdf"
21
+ # Prawn::Document.generate(file, :skip_page_creation => true) do
22
+ # point = [bounds.right-50, bounds.bottom + 25]
23
+ # page_counter = lazy_bounding_box(point, :width => 50) do
24
+ # text "Page: #{page_count}"
25
+ # end
26
+ #
27
+ # 10.times do
28
+ # start_new_page
29
+ # text "Some text"
30
+ # page_counter.draw
31
+ # end
32
+ # end
33
+ #
34
+ def lazy_bounding_box(*args,&block)
35
+ map_to_absolute!(args[0])
36
+ box = LazyBoundingBox.new(self,*args)
37
+ box.action(&block)
38
+ return box
39
+ end
40
+
41
+ # A bounding box with the same dimensions of its parents, minus a margin
42
+ # on all sides
43
+ #
44
+ def padded_box(margin, &block)
45
+ bounding_box [bounds.left + margin, bounds.top - margin],
46
+ :width => bounds.width - (margin * 2),
47
+ :height => bounds.height - (margin * 2), &block
48
+ end
49
+
50
+ class LazyBoundingBox < BoundingBox
51
+
52
+ # Defines the block to be executed by LazyBoundingBox#draw.
53
+ # Usually, this will be used via a higher level interface.
54
+ # See the documentation for Document#lazy_bounding_box, Document#header,
55
+ # and Document#footer
56
+ #
57
+ def action(&block)
58
+ @action = block
59
+ end
60
+
61
+ # Sets Document#bounds to use the LazyBoundingBox for its bounds,
62
+ # runs the block specified by LazyBoundingBox#action,
63
+ # and then restores the original bounds of the document.
64
+ #
65
+ def draw
66
+ @parent.mask(:y) do
67
+ parent_box = @parent.bounds
68
+ @parent.bounds = self
69
+ @parent.y = absolute_top
70
+ @action.call
71
+ @parent.bounds = parent_box
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,416 @@
1
+ # encoding: utf-8
2
+ #
3
+ # table.rb : Simple table drawing functionality
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
+
9
+ require "prawn/table/cell"
10
+
11
+ module Prawn
12
+ class Document
13
+
14
+ # Builds and renders a Document::Table object from raw data.
15
+ # For details on the options that can be passed, see
16
+ # Document::Table.new
17
+ #
18
+ # data = [["Gregory","Brown"],["James","Healy"],["Jia","Wu"]]
19
+ #
20
+ # Prawn::Document.generate("table.pdf") do
21
+ #
22
+ # # Default table, without headers
23
+ # table(data)
24
+ #
25
+ # # Default table with headers
26
+ # table data, :headers => ["First Name", "Last Name"]
27
+ #
28
+ # # Very close to PDF::Writer's default SimpleTable output
29
+ # table data, :headers => ["First Name", "Last Name"],
30
+ # :font_size => 10,
31
+ # :vertical_padding => 2,
32
+ # :horizontal_padding => 5,
33
+ # :position => :center,
34
+ # :row_colors => :pdf_writer,
35
+ #
36
+ # # Grid border style with explicit column widths.
37
+ # table data, :border_style => :grid,
38
+ # :column_widths => { 0 => 100, 1 => 150 }
39
+ #
40
+ # end
41
+ #
42
+ # Will raise <tt>Prawn::Errors::EmptyTable</tt> given
43
+ # a nil or empty <tt>data</tt> paramater.
44
+ #
45
+ def table(data, options={})
46
+ if data.nil? || data.empty?
47
+ raise Prawn::Errors::EmptyTable,
48
+ "data must be a non-empty, non-nil, two dimensional array " +
49
+ "of Prawn::Cells or strings"
50
+ end
51
+ Prawn::Table.new(data,self,options).draw
52
+ end
53
+ end
54
+
55
+
56
+ # This class implements simple PDF table generation.
57
+ #
58
+ # Prawn tables have the following features:
59
+ #
60
+ # * Can be generated with or without headers
61
+ # * Can tweak horizontal and vertical padding of text
62
+ # * Minimal styling support (borders / row background colors)
63
+ # * Can be positioned by bounding boxes (left/center aligned) or an
64
+ # absolute x position
65
+ # * Automated page-breaking as needed
66
+ # * Column widths can be calculated automatically or defined explictly on a
67
+ # column by column basis
68
+ # * Text alignment can be set for the whole table or by column
69
+ #
70
+ # The current implementation is a bit barebones, but covers most of the
71
+ # basic needs for PDF table generation. If you have feature requests,
72
+ # please share them at: http://groups.google.com/group/prawn-ruby
73
+ #
74
+ # Tables will be revisited before the end of the Ruby Mendicant project and
75
+ # the most commonly needed functionality will likely be added.
76
+ #
77
+ class Table
78
+
79
+ include Prawn::Configurable
80
+
81
+ attr_reader :column_widths # :nodoc:
82
+
83
+ NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?$/ #:nodoc:
84
+
85
+ # Creates a new Document::Table object. This is generally called
86
+ # indirectly through Document#table but can also be used explictly.
87
+ #
88
+ # The <tt>data</tt> argument is a two dimensional array of strings,
89
+ # organized by row, e.g. [["r1-col1","r1-col2"],["r2-col2","r2-col2"]].
90
+ # As with all Prawn text drawing operations, strings must be UTF-8 encoded.
91
+ #
92
+ # The following options are available for customizing your tables, with
93
+ # defaults shown in [] at the end of each description.
94
+ #
95
+ # <tt>:headers</tt>:: An array of table headers, either strings or Cells. [Empty]
96
+ # <tt>:align_headers</tt>:: Alignment of header text. Specify for entire header (<tt>:left</tt>) or by column (<tt>{ 0 => :right, 1 => :left}</tt>). If omitted, the header alignment is the same as the column alignment.
97
+ # <tt>:header_text_color</tt>:: Sets the text color of the headers
98
+ # <tt>:header_color</tt>:: Manually sets the header color
99
+ # <tt>:font_size</tt>:: The font size for the text cells . [12]
100
+ # <tt>:horizontal_padding</tt>:: The horizontal cell padding in PDF points [5]
101
+ # <tt>:vertical_padding</tt>:: The vertical cell padding in PDF points [5]
102
+ # <tt>:padding</tt>:: Horizontal and vertical cell padding (overrides both)
103
+ # <tt>:border_width</tt>:: With of border lines in PDF points [1]
104
+ # <tt>:border_style</tt>:: If set to :grid, fills in all borders. If set to :underline_header, underline header only. Otherwise, borders are drawn on columns only, not rows
105
+ # <tt>:border_color</tt>:: Sets the color of the borders.
106
+ # <tt>:position</tt>:: One of <tt>:left</tt>, <tt>:center</tt> or <tt>n</tt>, where <tt>n</tt> is an x-offset from the left edge of the current bounding box
107
+ # <tt>:width</tt>:: A set width for the table, defaults to the sum of all column widths
108
+ # <tt>:column_widths</tt>:: A hash of indices and widths in PDF points. E.g. <tt>{ 0 => 50, 1 => 100 }</tt>
109
+ # <tt>:row_colors</tt>:: Used to specify background colors for rows. See below for usage.
110
+ # <tt>:align</tt>:: Alignment of text in columns, for entire table (<tt>:center</tt>) or by column (<tt>{ 0 => :left, 1 => :center}</tt>)
111
+ #
112
+ # Row colors (<tt>:row_colors</tt>) are specified as HTML hex color values,
113
+ # e.g., "ccaaff". They can take several forms:
114
+ #
115
+ # * An array of colors, used cyclically to "zebra stripe" the table: <tt>['ffffff', 'cccccc', '336699']</tt>.
116
+ # * A hash taking 0-based row numbers to colors: <tt>{ 0 => 'ffffff', 2 => 'cccccc'}</tt>.
117
+ # * The symbol <tt>:pdf_writer</tt>, for PDF::Writer's default color scheme.
118
+ #
119
+ # See Document#table for typical usage, as directly using this class is
120
+ # not recommended unless you know why you want to do it.
121
+ #
122
+ def initialize(data, document, options={})
123
+ unless data.all? { |e| Array === e }
124
+ raise Prawn::Errors::InvalidTableData,
125
+ "data must be a two dimensional array of Prawn::Cells or strings"
126
+ end
127
+
128
+ @data = data
129
+ @document = document
130
+
131
+ Prawn.verify_options [:font_size,:border_style, :border_width,
132
+ :position, :headers, :row_colors, :align, :align_headers,
133
+ :header_text_color, :border_color, :horizontal_padding,
134
+ :vertical_padding, :padding, :column_widths, :width, :header_color ],
135
+ options
136
+
137
+ configuration.update(options)
138
+
139
+ if padding = options[:padding]
140
+ C(:horizontal_padding => padding, :vertical_padding => padding)
141
+ end
142
+
143
+ if options[:row_colors] == :pdf_writer
144
+ C(:row_colors => ["ffffff","cccccc"])
145
+ end
146
+
147
+ if options[:row_colors]
148
+ C(:original_row_colors => C(:row_colors))
149
+ end
150
+
151
+ calculate_column_widths(options[:column_widths], options[:width])
152
+ end
153
+
154
+ attr_reader :column_widths #:nodoc:
155
+
156
+ # Width of the table in PDF points
157
+ #
158
+ def width
159
+ @column_widths.inject(0) { |s,r| s + r }
160
+ end
161
+
162
+ # Draws the table onto the PDF document
163
+ #
164
+ def draw
165
+ @parent_bounds = @document.bounds
166
+ case C(:position)
167
+ when :center
168
+ x = (@document.bounds.width - width) / 2.0
169
+ dy = @document.bounds.absolute_top - @document.y
170
+ final_pos = nil
171
+
172
+ @document.bounding_box [x, @parent_bounds.top], :width => width do
173
+ @document.move_down(dy)
174
+ generate_table
175
+ final_pos = @document.y
176
+ end
177
+
178
+ @document.y = final_pos
179
+ when Numeric
180
+ x, y = C(:position), @document.cursor
181
+ final_pos = nil
182
+
183
+ @document.bounding_box([x,y], :width => width) do
184
+ generate_table
185
+ final_pos = @document.y
186
+ end
187
+
188
+ @document.y = final_pos
189
+ else
190
+ generate_table
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def default_configuration
197
+ { :font_size => 12,
198
+ :border_width => 1,
199
+ :position => :left,
200
+ :horizontal_padding => 5,
201
+ :vertical_padding => 5 }
202
+ end
203
+
204
+ def calculate_column_widths(manual_widths=nil, width=nil)
205
+ @column_widths = [0] * @data[0].inject(0){ |acc, e|
206
+ acc += (e.is_a?(Hash) && e.has_key?(:colspan)) ? e[:colspan] : 1 }
207
+
208
+ renderable_data.each do |row|
209
+ colspan = 0
210
+ row.each_with_index do |cell,i|
211
+ cell_text = cell.is_a?(Hash) ? cell[:text] : cell.to_s
212
+ length = cell_text.lines.map { |e|
213
+ @document.width_of(e, :size => C(:font_size)) }.max.to_f +
214
+ 2*C(:horizontal_padding)
215
+ if length > @column_widths[i+colspan]
216
+ @column_widths[i+colspan] = length.ceil
217
+ end
218
+
219
+ if cell.is_a?(Hash) && cell[:colspan]
220
+ colspan += cell[:colspan] - 1
221
+ end
222
+ end
223
+ end
224
+
225
+ fit_within_bounds(manual_widths, width)
226
+ end
227
+
228
+ def fit_within_bounds(manual_widths, width)
229
+ manual_width = 0
230
+ manual_widths.each { |k,v|
231
+ @column_widths[k] = v; manual_width += v } if manual_widths
232
+
233
+ #Ensures that the maximum width of the document is not exceeded
234
+ #Takes into consideration the manual widths specified (With full manual
235
+ # widths specified, the width can exceed the document width as manual
236
+ # widths are taken as gospel)
237
+ max_width = width || @document.margin_box.width
238
+ calculated_width = @column_widths.inject {|sum,e| sum += e }
239
+
240
+ if calculated_width > max_width
241
+ shrink_by = (max_width - manual_width).to_f /
242
+ (calculated_width - manual_width)
243
+ @column_widths.each_with_index { |c,i|
244
+ @column_widths[i] = c * shrink_by if manual_widths.nil? ||
245
+ manual_widths[i].nil?
246
+ }
247
+ elsif width && calculated_width < width
248
+ grow_by = (width - manual_width).to_f /
249
+ (calculated_width - manual_width)
250
+ @column_widths.each_with_index { |c,i|
251
+ @column_widths[i] = c * grow_by if manual_widths.nil? ||
252
+ manual_widths[i].nil?
253
+ }
254
+ end
255
+ end
256
+
257
+
258
+ def renderable_data
259
+ C(:headers) ? [C(:headers)] + @data : @data
260
+ end
261
+
262
+ def generate_table
263
+ page_contents = []
264
+ y_pos = @document.y
265
+
266
+ @document.font_size C(:font_size) do
267
+ renderable_data.each_with_index do |row,index|
268
+ c = Prawn::Table::CellBlock.new(@document)
269
+
270
+ if C(:row_colors).is_a?(Hash)
271
+ real_index = index
272
+ real_index -= 1 if C(:headers)
273
+ color = C(:row_colors)[real_index]
274
+ c.background_color = color if color
275
+ end
276
+
277
+ col_index = 0
278
+ row.each do |e|
279
+ case C(:align)
280
+ when Hash
281
+ align = C(:align)[col_index]
282
+ else
283
+ align = C(:align)
284
+ end
285
+
286
+
287
+ align ||= e.to_s =~ NUMBER_PATTERN ? :right : :left
288
+
289
+ case e
290
+ when Prawn::Table::Cell
291
+ e.document = @document
292
+ e.width = @column_widths[col_index]
293
+ e.horizontal_padding = C(:horizontal_padding)
294
+ e.vertical_padding = C(:vertical_padding)
295
+ e.border_width = C(:border_width)
296
+ e.border_style = :sides
297
+ e.align = align
298
+ c << e
299
+ else
300
+ text = e.is_a?(Hash) ? e[:text] : e.to_s
301
+ width = if e.is_a?(Hash) && e.has_key?(:colspan)
302
+ @column_widths.slice(col_index, e[:colspan]).inject {
303
+ |sum, width| sum + width }
304
+ else
305
+ @column_widths[col_index]
306
+ end
307
+
308
+ cell_options = {
309
+ :document => @document,
310
+ :text => text,
311
+ :width => width,
312
+ :horizontal_padding => C(:horizontal_padding),
313
+ :vertical_padding => C(:vertical_padding),
314
+ :border_width => C(:border_width),
315
+ :border_style => :sides,
316
+ :align => align
317
+ }
318
+
319
+ if e.is_a?(Hash)
320
+ opts = e.dup
321
+ opts.delete(:colspan)
322
+ cell_options.update(opts)
323
+ end
324
+
325
+ c << Prawn::Table::Cell.new(cell_options)
326
+ end
327
+
328
+ col_index += (e.is_a?(Hash) && e.has_key?(:colspan)) ? e[:colspan] : 1
329
+ end
330
+
331
+ bbox = @parent_bounds.stretchy? ? @document.margin_box : @parent_bounds
332
+ if c.height > y_pos - bbox.absolute_bottom
333
+ if C(:headers) && page_contents.length == 1
334
+ @parent_bounds.move_past_bottom
335
+ y_pos = @document.y
336
+ else
337
+ draw_page(page_contents)
338
+ @parent_bounds.move_past_bottom
339
+ if C(:headers) && page_contents.any?
340
+ page_contents = [page_contents[0]]
341
+ y_pos = @document.y - page_contents[0].height
342
+ else
343
+ page_contents = []
344
+ y_pos = @document.y
345
+ end
346
+ end
347
+ end
348
+
349
+ page_contents << c
350
+
351
+ y_pos -= c.height
352
+
353
+ if index == renderable_data.length - 1
354
+ draw_page(page_contents)
355
+ end
356
+
357
+ end
358
+ end
359
+ end
360
+
361
+ def draw_page(contents)
362
+ return if contents.empty?
363
+
364
+ if C(:border_style) == :underline_header
365
+ contents.each { |e| e.border_style = :none }
366
+ contents.first.border_style = :bottom_only if C(:headers)
367
+ elsif C(:border_style) == :grid || contents.length == 1
368
+ contents.each { |e| e.border_style = :all }
369
+ else
370
+ contents.first.border_style = C(:headers) ? :all : :no_bottom
371
+ contents.last.border_style = :no_top
372
+ end
373
+
374
+ if C(:headers)
375
+ contents.first.cells.each_with_index do |e,i|
376
+ if C(:align_headers)
377
+ case C(:align_headers)
378
+ when Hash
379
+ align = C(:align_headers)[i]
380
+ else
381
+ align = C(:align_headers)
382
+ end
383
+ end
384
+ e.align = align if align
385
+ e.text_color = C(:header_text_color) if C(:header_text_color)
386
+ e.background_color = C(:header_color) if C(:header_color)
387
+ end
388
+ end
389
+
390
+ contents.each do |x|
391
+ unless x.background_color
392
+ x.background_color = next_row_color if C(:row_colors)
393
+ end
394
+ x.border_color = C(:border_color) if C(:border_color)
395
+
396
+ x.draw
397
+ end
398
+
399
+ reset_row_colors
400
+ end
401
+
402
+
403
+ def next_row_color
404
+ return if C(:row_colors).is_a?(Hash)
405
+
406
+ color = C(:row_colors).shift
407
+ C(:row_colors).push(color)
408
+ color
409
+ end
410
+
411
+ def reset_row_colors
412
+ C(:row_colors => C(:original_row_colors).dup) if C(:row_colors)
413
+ end
414
+
415
+ end
416
+ end