prawn-table 0.0.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +340 -0
  4. data/GPLv3 +674 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE +56 -0
  7. data/lib/prawn/table.rb +641 -0
  8. data/lib/prawn/table/cell.rb +772 -0
  9. data/lib/prawn/table/cell/image.rb +69 -0
  10. data/lib/prawn/table/cell/in_table.rb +33 -0
  11. data/lib/prawn/table/cell/span_dummy.rb +93 -0
  12. data/lib/prawn/table/cell/subtable.rb +66 -0
  13. data/lib/prawn/table/cell/text.rb +154 -0
  14. data/lib/prawn/table/cells.rb +255 -0
  15. data/lib/prawn/table/column_width_calculator.rb +182 -0
  16. data/manual/contents.rb +13 -0
  17. data/manual/example_helper.rb +8 -0
  18. data/manual/table/basic_block.rb +53 -0
  19. data/manual/table/before_rendering_page.rb +26 -0
  20. data/manual/table/cell_border_lines.rb +24 -0
  21. data/manual/table/cell_borders_and_bg.rb +31 -0
  22. data/manual/table/cell_dimensions.rb +30 -0
  23. data/manual/table/cell_text.rb +38 -0
  24. data/manual/table/column_widths.rb +30 -0
  25. data/manual/table/content_and_subtables.rb +39 -0
  26. data/manual/table/creation.rb +27 -0
  27. data/manual/table/filtering.rb +36 -0
  28. data/manual/table/flow_and_header.rb +17 -0
  29. data/manual/table/image_cells.rb +33 -0
  30. data/manual/table/position.rb +29 -0
  31. data/manual/table/row_colors.rb +20 -0
  32. data/manual/table/span.rb +30 -0
  33. data/manual/table/style.rb +22 -0
  34. data/manual/table/table.rb +52 -0
  35. data/manual/table/width.rb +27 -0
  36. data/prawn-table.gemspec +48 -0
  37. data/spec/cell_spec.rb +629 -0
  38. data/spec/extensions/encoding_helpers.rb +11 -0
  39. data/spec/extensions/mocha.rb +46 -0
  40. data/spec/spec_helper.rb +53 -0
  41. data/spec/table/span_dummy_spec.rb +17 -0
  42. data/spec/table_spec.rb +1527 -0
  43. metadata +240 -0
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ # image.rb: Table image cells.
4
+ #
5
+ # Copyright September 2010, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ module Prawn
9
+ class Table
10
+ class Cell
11
+
12
+ # @private
13
+ class Image < Cell
14
+
15
+ def initialize(pdf, point, options={})
16
+ @image_options = {}
17
+ super
18
+
19
+ @pdf_object, @image_info = @pdf.build_image_object(@file)
20
+ @natural_width, @natural_height = @image_info.calc_image_dimensions(
21
+ @image_options)
22
+ end
23
+
24
+ def image=(file)
25
+ @file = file
26
+ end
27
+
28
+ def scale=(s)
29
+ @image_options[:scale] = s
30
+ end
31
+
32
+ def fit=(f)
33
+ @image_options[:fit] = f
34
+ end
35
+
36
+ def image_height=(h)
37
+ @image_options[:height] = h
38
+ end
39
+
40
+ def image_width=(w)
41
+ @image_options[:width] = w
42
+ end
43
+
44
+ def position=(p)
45
+ @image_options[:position] = p
46
+ end
47
+
48
+ def vposition=(vp)
49
+ @image_options[:vposition] = vp
50
+ end
51
+
52
+ def natural_content_width
53
+ @natural_width
54
+ end
55
+
56
+ def natural_content_height
57
+ @natural_height
58
+ end
59
+
60
+ # Draw the image on the page.
61
+ #
62
+ def draw_content
63
+ @pdf.embed_image(@pdf_object, @image_info, @image_options)
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ # Accessors for using a Cell inside a Table.
4
+ #
5
+ # Contributed by Brad Ediger.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Table
11
+
12
+ class Cell
13
+
14
+ # This module extends Cell objects when they are used in a table (as
15
+ # opposed to standalone). Its properties apply to cells-in-tables but not
16
+ # cells themselves.
17
+ #
18
+ # @private
19
+ module InTable
20
+
21
+ # Row number (0-based).
22
+ #
23
+ attr_accessor :row
24
+
25
+ # Column number (0-based).
26
+ #
27
+ attr_accessor :column
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ # span_dummy.rb: Placeholder for non-master spanned cells.
4
+ #
5
+ # Copyright December 2011, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ module Prawn
9
+ class Table
10
+ class Cell
11
+
12
+ # A Cell object used to represent all but the topmost cell in a span
13
+ # group.
14
+ #
15
+ # @private
16
+ class SpanDummy < Cell
17
+ def initialize(pdf, master_cell)
18
+ super(pdf, [0, pdf.cursor])
19
+ @master_cell = master_cell
20
+ @padding = [0, 0, 0, 0]
21
+ end
22
+
23
+ # By default, a span dummy will never increase the height demand.
24
+ #
25
+ def natural_content_height
26
+ 0
27
+ end
28
+
29
+ # By default, a span dummy will never increase the width demand.
30
+ #
31
+ def natural_content_width
32
+ 0
33
+ end
34
+
35
+ def avg_spanned_min_width
36
+ @master_cell.avg_spanned_min_width
37
+ end
38
+
39
+ # Dummy cells have nothing to draw.
40
+ #
41
+ def draw_borders(pt)
42
+ end
43
+
44
+ # Dummy cells have nothing to draw.
45
+ #
46
+ def draw_bounded_content(pt)
47
+ end
48
+
49
+ def padding_right=(val)
50
+ @master_cell.padding_right = val if rightmost?
51
+ end
52
+
53
+ def padding_bottom=(val)
54
+ @master_cell.padding_bottom = val if bottommost?
55
+ end
56
+
57
+ def border_right_color=(val)
58
+ @master_cell.border_right_color = val if rightmost?
59
+ end
60
+
61
+ def border_bottom_color=(val)
62
+ @master_cell.border_bottom_color = val if bottommost?
63
+ end
64
+
65
+ def border_right_width=(val)
66
+ @master_cell.border_right_width = val if rightmost?
67
+ end
68
+
69
+ def border_bottom_width=(val)
70
+ @master_cell.border_bottom_width = val if bottommost?
71
+ end
72
+
73
+ def background_color
74
+ @master_cell.background_color
75
+ end
76
+
77
+ private
78
+
79
+ # Are we on the right border of the span?
80
+ #
81
+ def rightmost?
82
+ @column == @master_cell.column + @master_cell.colspan - 1
83
+ end
84
+
85
+ # Are we on the bottom border of the span?
86
+ #
87
+ def bottommost?
88
+ @row == @master_cell.row + @master_cell.rowspan - 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+
3
+ # subtable.rb: Yo dawg.
4
+ #
5
+ # Copyright January 2010, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ module Prawn
9
+ class Table
10
+ class Cell
11
+
12
+ # A Cell that contains another table.
13
+ #
14
+ # @private
15
+ class Subtable < Cell
16
+
17
+ attr_reader :subtable
18
+
19
+ def initialize(pdf, point, options={})
20
+ super
21
+ @subtable = options[:content]
22
+
23
+ # Subtable padding defaults to zero
24
+ @padding = [0, 0, 0, 0]
25
+ end
26
+
27
+ # Sets the text color of the entire subtable.
28
+ #
29
+ def text_color=(color)
30
+ @subtable.cells.text_color = color
31
+ end
32
+
33
+ # Proxied to subtable.
34
+ #
35
+ def natural_content_width
36
+ @subtable.cells.width
37
+ end
38
+
39
+ # Proxied to subtable.
40
+ #
41
+ def min_width
42
+ @subtable.cells.min_width
43
+ end
44
+
45
+ # Proxied to subtable.
46
+ #
47
+ def max_width
48
+ @subtable.cells.max_width
49
+ end
50
+
51
+ # Proxied to subtable.
52
+ #
53
+ def natural_content_height
54
+ @subtable.cells.height
55
+ end
56
+
57
+ # Draws the subtable.
58
+ #
59
+ def draw_content
60
+ @subtable.draw
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,154 @@
1
+ # encoding: utf-8
2
+
3
+ # text.rb: Text table cells.
4
+ #
5
+ # Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ module Prawn
9
+ class Table
10
+ class Cell
11
+
12
+ # A Cell that contains text. Has some limited options to set font family,
13
+ # size, and style.
14
+ #
15
+ # @private
16
+ class Text < Cell
17
+
18
+ TextOptions = [:inline_format, :kerning, :size, :align, :valign,
19
+ :rotate, :rotate_around, :leading, :single_line, :skip_encoding,
20
+ :overflow, :min_font_size]
21
+
22
+ TextOptions.each do |option|
23
+ define_method("#{option}=") { |v| @text_options[option] = v }
24
+ define_method(option) { @text_options[option] }
25
+ end
26
+
27
+ attr_writer :font, :text_color
28
+
29
+ def initialize(pdf, point, options={})
30
+ @text_options = {}
31
+ super
32
+ end
33
+
34
+ # Returns the font that will be used to draw this cell.
35
+ #
36
+ def font
37
+ with_font { @pdf.font }
38
+ end
39
+
40
+ # Sets the style of the font in use. Equivalent to the Text::Box
41
+ # +style+ option, but we already have a style method.
42
+ #
43
+ def font_style=(style)
44
+ @text_options[:style] = style
45
+ end
46
+
47
+ # Returns the width of this text with no wrapping. This will be far off
48
+ # from the final width if the text is long.
49
+ #
50
+ def natural_content_width
51
+ @natural_content_width ||= [styled_width_of(@content), @pdf.bounds.width].min
52
+ end
53
+
54
+ # Returns the natural height of this block of text, wrapped to the
55
+ # preset width.
56
+ #
57
+ def natural_content_height
58
+ with_font do
59
+ b = text_box(:width => spanned_content_width + FPTolerance)
60
+ b.render(:dry_run => true)
61
+ b.height + b.line_gap
62
+ end
63
+ end
64
+
65
+ # Draws the text content into its bounding box.
66
+ #
67
+ def draw_content
68
+ with_font do
69
+ @pdf.move_down((@pdf.font.line_gap + @pdf.font.descender)/2)
70
+ with_text_color do
71
+ text_box(:width => spanned_content_width + FPTolerance,
72
+ :height => spanned_content_height + FPTolerance,
73
+ :at => [0, @pdf.cursor]).render
74
+ end
75
+ end
76
+ end
77
+
78
+ def set_width_constraints
79
+ # Sets a reasonable minimum width. If the cell has any content, make
80
+ # sure we have enough width to be at least one character wide. This is
81
+ # a bit of a hack, but it should work well enough.
82
+ unless defined?(@min_width) && @min_width
83
+ min_content_width = [natural_content_width, styled_width_of_single_character].min
84
+ @min_width = padding_left + padding_right + min_content_width
85
+ super
86
+ end
87
+ end
88
+
89
+ protected
90
+
91
+ def with_font
92
+ @pdf.save_font do
93
+ options = {}
94
+ options[:style] = @text_options[:style] if @text_options[:style]
95
+ options[:style] ||= @pdf.font.options[:style] if @pdf.font.options[:style]
96
+
97
+ @pdf.font(defined?(@font) && @font || @pdf.font.family, options)
98
+
99
+ yield
100
+ end
101
+ end
102
+
103
+ def with_text_color
104
+ if defined?(@text_color) && @text_color
105
+ begin
106
+ old_color = @pdf.fill_color || '000000'
107
+ @pdf.fill_color(@text_color)
108
+ yield
109
+ ensure
110
+ @pdf.fill_color(old_color)
111
+ end
112
+ else
113
+ yield
114
+ end
115
+ end
116
+
117
+ def text_box(extra_options={})
118
+ if p = @text_options[:inline_format]
119
+ p = [] unless p.is_a?(Array)
120
+ options = @text_options.dup
121
+ options.delete(:inline_format)
122
+ options.merge!(extra_options)
123
+ options[:document] = @pdf
124
+
125
+ array = @pdf.text_formatter.format(@content, *p)
126
+ ::Prawn::Text::Formatted::Box.new(array,
127
+ options.merge(extra_options).merge(:document => @pdf))
128
+ else
129
+ ::Prawn::Text::Box.new(@content, @text_options.merge(extra_options).
130
+ merge(:document => @pdf))
131
+ end
132
+ end
133
+
134
+ # Returns the width of +text+ under the given text options.
135
+ #
136
+ def styled_width_of(text)
137
+ @pdf.width_of(text, @text_options)
138
+ end
139
+
140
+ private
141
+
142
+ # Returns the greatest possible width of any single character
143
+ # under the given text options.
144
+ # (We use this to determine the minimum width of a table cell)
145
+ # (Although we currently determine this by measuring "M", it should really
146
+ # use whichever character is widest under the current font)
147
+ #
148
+ def styled_width_of_single_character
149
+ styled_width_of("M")
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,255 @@
1
+ # encoding: utf-8
2
+
3
+ # cells.rb: Methods for accessing rows, columns, and cells of a Prawn::Table.
4
+ #
5
+ # Copyright December 2009, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Table
11
+ # Selects the given rows (0-based) for styling. Returns a Cells object --
12
+ # see the documentation on Cells for things you can do with cells.
13
+ #
14
+ def rows(row_spec)
15
+ cells.rows(row_spec)
16
+ end
17
+ alias_method :row, :rows
18
+
19
+ # Selects the given columns (0-based) for styling. Returns a Cells object
20
+ # -- see the documentation on Cells for things you can do with cells.
21
+ #
22
+ def columns(col_spec)
23
+ cells.columns(col_spec)
24
+ end
25
+ alias_method :column, :columns
26
+
27
+ # Represents a selection of cells to be styled. Operations on a CellProxy
28
+ # can be chained, and cell properties can be set one-for-all on the proxy.
29
+ #
30
+ # To set vertical borders only:
31
+ #
32
+ # table.cells.borders = [:left, :right]
33
+ #
34
+ # To highlight a rectangular area of the table:
35
+ #
36
+ # table.rows(1..3).columns(2..4).background_color = 'ff0000'
37
+ #
38
+ class Cells < Array
39
+
40
+ # @group Experimental API
41
+
42
+ # Limits selection to the given row or rows. +row_spec+ can be anything
43
+ # that responds to the === operator selecting a set of 0-based row
44
+ # numbers; most commonly a number or a range.
45
+ #
46
+ # table.row(0) # selects first row
47
+ # table.rows(3..4) # selects rows four and five
48
+ #
49
+ def rows(row_spec)
50
+ index_cells unless defined?(@indexed) && @indexed
51
+ row_spec = transform_spec(row_spec, @first_row, @row_count)
52
+ Cells.new(@rows[row_spec] ||= select { |c|
53
+ row_spec.respond_to?(:include?) ?
54
+ row_spec.include?(c.row) : row_spec === c.row })
55
+ end
56
+ alias_method :row, :rows
57
+
58
+ # Returns the number of rows in the list.
59
+ #
60
+ def row_count
61
+ index_cells unless defined?(@indexed) && @indexed
62
+ @row_count
63
+ end
64
+
65
+ # Limits selection to the given column or columns. +col_spec+ can be
66
+ # anything that responds to the === operator selecting a set of 0-based
67
+ # column numbers; most commonly a number or a range.
68
+ #
69
+ # table.column(0) # selects first column
70
+ # table.columns(3..4) # selects columns four and five
71
+ #
72
+ def columns(col_spec)
73
+ index_cells unless defined?(@indexed) && @indexed
74
+ col_spec = transform_spec(col_spec, @first_column, @column_count)
75
+ Cells.new(@columns[col_spec] ||= select { |c|
76
+ col_spec.respond_to?(:include?) ?
77
+ col_spec.include?(c.column) : col_spec === c.column })
78
+ end
79
+ alias_method :column, :columns
80
+
81
+ # Returns the number of columns in the list.
82
+ #
83
+ def column_count
84
+ index_cells unless defined?(@indexed) && @indexed
85
+ @column_count
86
+ end
87
+
88
+ # Allows you to filter the given cells by arbitrary properties.
89
+ #
90
+ # table.column(4).filter { |cell| cell.content =~ /Yes/ }.
91
+ # background_color = '00ff00'
92
+ #
93
+ def filter(&block)
94
+ Cells.new(select(&block))
95
+ end
96
+
97
+ # Retrieves a cell based on its 0-based row and column. Returns an
98
+ # individual Cell, not a Cells collection.
99
+ #
100
+ # table.cells[0, 0].content # => "First cell content"
101
+ #
102
+ def [](row, col)
103
+ return nil if empty?
104
+ index_cells unless defined?(@indexed) && @indexed
105
+ row_array, col_array = @rows[@first_row + row] || [], @columns[@first_column + col] || []
106
+ if row_array.length < col_array.length
107
+ row_array.find { |c| c.column == @first_column + col }
108
+ else
109
+ col_array.find { |c| c.row == @first_row + row }
110
+ end
111
+ end
112
+
113
+ # Puts a cell in the collection at the given position. Internal use only.
114
+ #
115
+ def []=(row, col, cell) # :nodoc:
116
+ cell.extend(Cell::InTable)
117
+ cell.row = row
118
+ cell.column = col
119
+
120
+ if defined?(@indexed) && @indexed
121
+ (@rows[row] ||= []) << cell
122
+ (@columns[col] ||= []) << cell
123
+ @first_row = row if !@first_row || row < @first_row
124
+ @first_column = col if !@first_column || col < @first_column
125
+ @row_count = @rows.size
126
+ @column_count = @columns.size
127
+ end
128
+
129
+ self << cell
130
+ end
131
+
132
+ # Supports setting multiple properties at once.
133
+ #
134
+ # table.cells.style(:padding => 0, :border_width => 2)
135
+ #
136
+ # is the same as:
137
+ #
138
+ # table.cells.padding = 0
139
+ # table.cells.border_width = 2
140
+ #
141
+ # You can also pass a block, which will be called for each cell in turn.
142
+ # This allows you to set more complicated properties:
143
+ #
144
+ # table.cells.style { |cell| cell.border_width += 12 }
145
+ #
146
+ def style(options={}, &block)
147
+ each do |cell|
148
+ next if cell.is_a?(Cell::SpanDummy)
149
+ cell.style(options, &block)
150
+ end
151
+ end
152
+
153
+ # Returns the total width of all columns in the selected set.
154
+ #
155
+ def width
156
+ widths = {}
157
+ each do |cell|
158
+ per_cell_width = cell.width_ignoring_span.to_f / cell.colspan
159
+ cell.colspan.times do |n|
160
+ widths[cell.column+n] = [widths[cell.column+n], per_cell_width].
161
+ compact.max
162
+ end
163
+ end
164
+ widths.values.inject(0, &:+)
165
+ end
166
+
167
+ # Returns minimum width required to contain cells in the set.
168
+ #
169
+ def min_width
170
+ aggregate_cell_values(:column, :avg_spanned_min_width, :max)
171
+ end
172
+
173
+ # Returns maximum width that can contain cells in the set.
174
+ #
175
+ def max_width
176
+ aggregate_cell_values(:column, :max_width_ignoring_span, :max)
177
+ end
178
+
179
+ # Returns the total height of all rows in the selected set.
180
+ #
181
+ def height
182
+ aggregate_cell_values(:row, :height_ignoring_span, :max)
183
+ end
184
+
185
+ # Supports setting arbitrary properties on a group of cells.
186
+ #
187
+ # table.cells.row(3..6).background_color = 'cc0000'
188
+ #
189
+ def method_missing(id, *args, &block)
190
+ if id.to_s =~ /=\z/
191
+ each { |c| c.send(id, *args, &block) if c.respond_to?(id) }
192
+ else
193
+ super
194
+ end
195
+ end
196
+
197
+ protected
198
+
199
+ # Defers indexing until rows() or columns() is actually called on the
200
+ # Cells object. Without this, we would needlessly index the leaf nodes of
201
+ # the object graph, the ones that are only there to be iterated over.
202
+ #
203
+ # Make sure to call this before using @rows or @columns.
204
+ #
205
+ def index_cells
206
+ @rows = {}
207
+ @columns = {}
208
+
209
+ each do |cell|
210
+ @rows[cell.row] ||= []
211
+ @rows[cell.row] << cell
212
+
213
+ @columns[cell.column] ||= []
214
+ @columns[cell.column] << cell
215
+ end
216
+
217
+ @first_row = @rows.keys.min
218
+ @first_column = @columns.keys.min
219
+
220
+ @row_count = @rows.size
221
+ @column_count = @columns.size
222
+
223
+ @indexed = true
224
+ end
225
+
226
+ # Sum up a min/max value over rows or columns in the cells selected.
227
+ # Takes the min/max (per +aggregate+) of the result of sending +meth+ to
228
+ # each cell, grouped by +row_or_column+.
229
+ #
230
+ def aggregate_cell_values(row_or_column, meth, aggregate)
231
+ ColumnWidthCalculator.new(self).aggregate_cell_values(row_or_column, meth, aggregate)
232
+ end
233
+
234
+ # Transforms +spec+, a column / row specification, into an object that
235
+ # can be compared against a row or column number using ===. Normalizes
236
+ # negative indices to be positive, given a total size of +total+. The
237
+ # first row/column is indicated by +first+; this value is considered row
238
+ # or column 0.
239
+ #
240
+ def transform_spec(spec, first, total)
241
+ case spec
242
+ when Range
243
+ transform_spec(spec.begin, first, total) ..
244
+ transform_spec(spec.end, first, total)
245
+ when Integer
246
+ spec < 0 ? (first + total + spec) : first + spec
247
+ when Enumerable
248
+ spec.map { |x| first + x }
249
+ else # pass through
250
+ raise "Don't understand spec #{spec.inspect}"
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end