jyurek-prawn-layout 0.8.4

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