pdf-wrapper 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,116 @@
1
+ module PDF
2
+ class Wrapper
3
+
4
+ # draw a circle with radius r and a centre point at (x,y).
5
+ # Parameters:
6
+ # <tt>:x</tt>:: The x co-ordinate of the circle centre.
7
+ # <tt>:y</tt>:: The y co-ordinate of the circle centre.
8
+ # <tt>:r</tt>:: The radius of the circle
9
+ #
10
+ # Options:
11
+ # <tt>:color</tt>:: The colour of the circle outline
12
+ # <tt>:line_width</tt>:: The width of outline. Defaults to 0.5
13
+ # <tt>:fill_color</tt>:: The colour to fill the circle with. Defaults to nil (no fill)
14
+ def circle(x, y, r, options = {})
15
+ options.assert_valid_keys(:color, :line_width, :fill_color)
16
+
17
+ save_coords_and_state do
18
+ move_to(x + r, y)
19
+
20
+ # if the circle should be filled in
21
+ if options[:fill_color]
22
+ @context.save do
23
+ color(options[:fill_color])
24
+ @context.circle(x, y, r).fill
25
+ end
26
+ end
27
+
28
+ color(options[:color]) if options[:color]
29
+ line_width(options[:line_width]) if options[:line_width]
30
+ @context.circle(x, y, r).stroke
31
+ end
32
+ end
33
+
34
+ # draw a line from x1,y1 to x2,y2
35
+ #
36
+ # Options:
37
+ # <tt>:color</tt>:: The colour of the line
38
+ # <tt>:line_width</tt>:: The width of line. Defaults its 0.5
39
+ def line(x0, y0, x1, y1, options = {})
40
+ options.assert_valid_keys(:color, :line_width)
41
+
42
+ save_coords_and_state do
43
+ color(options[:color]) if options[:color]
44
+ line_width(options[:line_width]) if options[:line_width]
45
+ move_to(x0,y0)
46
+ @context.line_to(x1,y1).stroke
47
+ end
48
+ end
49
+
50
+ # change the default line width used to draw stroke on the canvas
51
+ #
52
+ # Parameters:
53
+ # <tt>f</tt>:: float value of stroke width from 0.01 to 255
54
+ def line_width(f)
55
+ @line_width = f
56
+ @context.set_line_width @context.device_to_user_distance(f,f).max
57
+ end
58
+ alias line_width= line_width
59
+
60
+ # Adds a cubic Bezier spline to the path from the (x0, y0) to position (x3, y3)
61
+ # in user-space coordinates, using (x1, y1) and (x2, y2) as the control points.
62
+ # Options:
63
+ # <tt>:color</tt>:: The colour of the line
64
+ # <tt>:line_width</tt>:: The width of line. Defaults to 0.5
65
+ def curve(x0, y0, x1, y1, x2, y2, x3, y3, options = {})
66
+ options.assert_valid_keys(:color, :line_width)
67
+
68
+ save_coords_and_state do
69
+ color(options[:color]) if options[:color]
70
+ line_width(options[:line_width]) if options[:line_width]
71
+ move_to(x0,y0)
72
+ @context.curve_to(x1, y1, x2, y2, x3, y3).stroke
73
+ end
74
+ end
75
+
76
+ # draw a rectangle starting at x,y with w,h dimensions.
77
+ # Parameters:
78
+ # <tt>:x</tt>:: The x co-ordinate of the top left of the rectangle.
79
+ # <tt>:y</tt>:: The y co-ordinate of the top left of the rectangle.
80
+ # <tt>:w</tt>:: The width of the rectangle
81
+ # <tt>:h</tt>:: The height of the rectangle
82
+ #
83
+ # Options:
84
+ # <tt>:color</tt>:: The colour of the rectangle outline
85
+ # <tt>:line_width</tt>:: The width of outline. Defaults to 0.5
86
+ # <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
87
+ # <tt>:radius</tt>:: If specified, the rectangle will have rounded corners with the specified radius
88
+ def rectangle(x, y, w, h, options = {})
89
+ options.assert_valid_keys(:color, :line_width, :fill_color, :radius)
90
+
91
+ save_coords_and_state do
92
+ # if the rectangle should be filled in
93
+ if options[:fill_color]
94
+ @context.save do
95
+ color(options[:fill_color])
96
+ if options[:radius]
97
+ @context.rounded_rectangle(x, y, w, h, options[:radius]).fill
98
+ else
99
+ @context.rectangle(x, y, w, h).fill
100
+ end
101
+ end
102
+ end
103
+
104
+ color(options[:color]) if options[:color]
105
+ line_width(options[:line_width]) if options[:line_width]
106
+
107
+ if options[:radius]
108
+ @context.rounded_rectangle(x, y, w, h, options[:radius]).stroke
109
+ else
110
+ @context.rectangle(x, y, w, h).stroke
111
+ end
112
+ end
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,206 @@
1
+ module PDF
2
+ class Wrapper
3
+ # add an image to the page - a wide range of image formats are supported,
4
+ # including svg, jpg, png and gif. PDF images are also supported - an attempt
5
+ # to add a multipage PDF will result in only the first page appearing in the
6
+ # new document.
7
+ #
8
+ # supported options:
9
+ # <tt>:left</tt>:: The x co-ordinate of the left-hand side of the image.
10
+ # <tt>:top</tt>:: The y co-ordinate of the top of the image.
11
+ # <tt>:height</tt>:: The height of the image
12
+ # <tt>:width</tt>:: The width of the image
13
+ # <tt>:proportional</tt>:: Boolean. Maintain image proportions when scaling. Defaults to false.
14
+ # <tt>:padding</tt>:: Add some padding between the image and the specified box.
15
+ # <tt>:center</tt>:: If the image is scaled, it will be centered horizontally and vertically
16
+ #
17
+ # left and top default to the current cursor location
18
+ # width and height default to the size of the imported image
19
+ # padding defaults to 0
20
+ def image(filename, opts = {})
21
+ # TODO: add some options for justification and padding
22
+ raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
23
+ opts.assert_valid_keys(default_positioning_options.keys + [:padding, :proportional, :center])
24
+
25
+ if opts[:padding]
26
+ opts[:left] += opts[:padding].to_i if opts[:left]
27
+ opts[:top] += opts[:padding].to_i if opts[:top]
28
+ opts[:width] -= opts[:padding].to_i * 2 if opts[:width]
29
+ opts[:height] -= opts[:padding].to_i * 2 if opts[:height]
30
+ end
31
+
32
+ case detect_image_type(filename)
33
+ when :pdf then draw_pdf filename, opts
34
+ when :png then draw_png filename, opts
35
+ when :svg then draw_svg filename, opts
36
+ else
37
+ draw_pixbuf filename, opts
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def detect_image_type(filename)
44
+ # read the first Kb from the file to attempt file type detection
45
+ f = File.new(filename)
46
+ bytes = f.read(1024)
47
+
48
+ # if the file is a PNG
49
+ if bytes[1,3].eql?("PNG")
50
+ return :png
51
+ elsif bytes[0,3].eql?("GIF")
52
+ return :gif
53
+ elsif bytes[0,4].eql?("%PDF")
54
+ return :pdf
55
+ elsif bytes.include?("<svg")
56
+ return :svg
57
+ elsif bytes.include?("Exif") || bytes.include?("JFIF")
58
+ return :jpg
59
+ else
60
+ return nil
61
+ end
62
+ end
63
+
64
+ # if need be, translate the x,y co-ords for an image to something different
65
+ #
66
+ # arguments:
67
+ # <tt>x</tt>:: The current x co-ord of the image
68
+ # <tt>y</tt>:: The current x co-ord of the image
69
+ # <tt>desired_w</tt>:: The image width requested by the user
70
+ # <tt>desired_h</tt>:: The image height requested by the user
71
+ # <tt>actual_w</tt>:: The width of the image we're going to draw
72
+ # <tt>actual_h</tt>:: The height of the image we're going to draw
73
+ # <tt>centre</tt>:: True if the image should be shifted to the center of it's box
74
+ def calc_image_coords(x, y, desired_w, desired_h, actual_w, actual_h, centre = false)
75
+
76
+ # if the width of the image is less than the requested box, calculate
77
+ # the white space buffer
78
+ if actual_w < desired_w && centre
79
+ white_space = desired_w - actual_w
80
+ x = x + (white_space / 2)
81
+ end
82
+
83
+ # if the height of the image is less than the requested box, calculate
84
+ # the white space buffer
85
+ if actual_h < desired_h && centre
86
+ white_space = desired_h - actual_h
87
+ y = y + (white_space / 2)
88
+ end
89
+
90
+ return x, y
91
+ end
92
+
93
+ # given a list of desired and actual image dimensions, calculate the
94
+ # size the image should actually be rendered at
95
+ def calc_image_dimensions(desired_w, desired_h, actual_w, actual_h, scale = false)
96
+ if scale
97
+ wp = desired_w / actual_w.to_f
98
+ hp = desired_h / actual_h.to_f
99
+
100
+ if wp < hp
101
+ width = actual_w * wp
102
+ height = actual_h * wp
103
+ else
104
+ width = actual_w * hp
105
+ height = actual_h * hp
106
+ end
107
+ else
108
+ width = desired_w || actual_w
109
+ height = desired_h || actual_h
110
+ end
111
+ return width.to_f, height.to_f
112
+ end
113
+
114
+ def draw_pdf(filename, opts = {})
115
+ # based on a similar function in rabbit. Thanks Kou.
116
+ load_libpoppler
117
+ x, y = current_point
118
+ page = Poppler::Document.new(filename).get_page(1)
119
+ w, h = page.size
120
+ width, height = calc_image_dimensions(opts[:width], opts[:height], w, h, opts[:proportional])
121
+ x, y = calc_image_coords(opts[:left] || x, opts[:top] || y, opts[:width] || w, opts[:height] || h, width, height, opts[:center])
122
+ @context.save do
123
+ @context.translate(x, y)
124
+ @context.scale(width / w, height / h)
125
+ @context.render_poppler_page(page)
126
+ end
127
+ move_to(opts[:left] || x, (opts[:top] || y) + height)
128
+ end
129
+
130
+ def draw_pixbuf(filename, opts = {})
131
+ # based on a similar function in rabbit. Thanks Kou.
132
+ load_libpixbuf
133
+ x, y = current_point
134
+ pixbuf = Gdk::Pixbuf.new(filename)
135
+ width, height = calc_image_dimensions(opts[:width], opts[:height], pixbuf.width, pixbuf.height, opts[:proportional])
136
+ x, y = calc_image_coords(opts[:left] || x, opts[:top] || y, opts[:width] || pixbuf.width, opts[:height] || pixbuf.height, width, height, opts[:center])
137
+ @context.save do
138
+ @context.translate(x, y)
139
+ @context.scale(width / pixbuf.width, height / pixbuf.height)
140
+ @context.set_source_pixbuf(pixbuf, 0, 0)
141
+ @context.paint
142
+ end
143
+ move_to(opts[:left] || x, (opts[:top] || y) + height)
144
+ rescue Gdk::PixbufError
145
+ raise ArgumentError, "Unrecognised image format (#{filename})"
146
+ end
147
+
148
+ def draw_png(filename, opts = {})
149
+ # based on a similar function in rabbit. Thanks Kou.
150
+ x, y = current_point
151
+ img_surface = Cairo::ImageSurface.from_png(filename)
152
+ width, height = calc_image_dimensions(opts[:width], opts[:height], img_surface.width, img_surface.height, opts[:proportional])
153
+ x, y = calc_image_coords(opts[:left] || x, opts[:top] || y, opts[:width] || img_surface.width, opts[:height] || img_surface.height, width, height, opts[:center])
154
+ @context.save do
155
+ @context.translate(x, y)
156
+ @context.scale(width / img_surface.width, height / img_surface.height)
157
+ @context.set_source(img_surface, 0, 0)
158
+ @context.paint
159
+ end
160
+ move_to(opts[:left] || x, (opts[:top] || y) + height)
161
+ end
162
+
163
+ def draw_svg(filename, opts = {})
164
+ # based on a similar function in rabbit. Thanks Kou.
165
+ load_librsvg
166
+ x, y = current_point
167
+ handle = RSVG::Handle.new_from_file(filename)
168
+ width, height = calc_image_dimensions(opts[:width], opts[:height], handle.width, handle.height, opts[:proportional])
169
+ x, y = calc_image_coords(opts[:left] || x, opts[:top] || y, opts[:width] || handle.width, opts[:height] || handle.height, width, height, opts[:center])
170
+ @context.save do
171
+ @context.translate(x, y)
172
+ @context.scale(width / handle.width, height / handle.height)
173
+ @context.render_rsvg_handle(handle)
174
+ #@context.paint
175
+ end
176
+ move_to(opts[:left] || x, (opts[:top] || y) + height)
177
+ end
178
+
179
+ def image_dimensions(filename)
180
+ raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
181
+
182
+ case detect_image_type(filename)
183
+ when :pdf then
184
+ load_libpoppler
185
+ page = Poppler::Document.new(filename).get_page(1)
186
+ return page.size
187
+ when :png then
188
+ img_surface = Cairo::ImageSurface.from_png(filename)
189
+ return img_surface.width, img_surface.height
190
+ when :svg then
191
+ load_librsvg
192
+ handle = RSVG::Handle.new_from_file(filename)
193
+ return handle.width, handle.height
194
+ else
195
+ load_libpixbuf
196
+ begin
197
+ pixbuf = Gdk::Pixbuf.new(filename)
198
+ return pixbuf.width, pixbuf.height
199
+ rescue Gdk::PixbufError
200
+ raise ArgumentError, "Unrecognised image format (#{filename})"
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,52 @@
1
+ module PDF
2
+ class Wrapper
3
+ # load libpango if it isn't already loaded.
4
+ # This will add some methods to the cairo Context class in addition to providing
5
+ # its own classes and constants. A small amount of documentation is available at
6
+ # http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#Pango+related+APIs
7
+ def load_libpango
8
+ begin
9
+ require 'pango' unless @context.respond_to? :create_pango_layout
10
+ rescue LoadError
11
+ raise LoadError, 'Ruby/Pango library not found. Visit http://ruby-gnome2.sourceforge.jp/'
12
+ end
13
+ end
14
+
15
+ # load lib gdkpixbuf if it isn't already loaded.
16
+ # This will add some methods to the cairo Context class in addition to providing
17
+ # its own classes and constants.
18
+ def load_libpixbuf
19
+ begin
20
+ require 'gdk_pixbuf2' unless @context.respond_to? :set_source_pixbuf
21
+ rescue LoadError
22
+ raise LoadError, 'Ruby/GdkPixbuf library not found. Visit http://ruby-gnome2.sourceforge.jp/'
23
+ end
24
+ end
25
+
26
+ # load lib poppler if it isn't already loaded.
27
+ # This will add some methods to the cairo Context class in addition to providing
28
+ # its own classes and constants.
29
+ def load_libpoppler
30
+ begin
31
+ require 'poppler' unless @context.respond_to? :render_poppler_page
32
+ rescue LoadError
33
+ raise LoadError, 'Ruby/Poppler library not found. Visit http://ruby-gnome2.sourceforge.jp/'
34
+ end
35
+ end
36
+
37
+ # load librsvg if it isn't already loaded
38
+ # This will add an additional method to the Cairo::Context class
39
+ # that allows an existing SVG to be drawn directly onto it
40
+ # There's a *little* bit of documentation at:
41
+ # http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#render_rsvg_handle
42
+ def load_librsvg
43
+ begin
44
+ require 'rsvg2' unless @context.respond_to? :render_svg_handle
45
+ rescue LoadError
46
+ raise LoadError, 'Ruby/RSVG library not found. Visit http://ruby-gnome2.sourceforge.jp/'
47
+ end
48
+ end
49
+
50
+
51
+ end
52
+ end
@@ -0,0 +1,473 @@
1
+ module PDF
2
+ class Wrapper
3
+
4
+ # Draws a basic table of text on the page. See the documentation for a detailed description of
5
+ # how to control the table and its appearance.
6
+ #
7
+ # <tt>data</tt>:: a 2d array with the data for the columns, or a PDF::Wrapper::Table object
8
+ #
9
+ # == Options
10
+ #
11
+ # The only options available when rendering a table are those relating to its size and location.
12
+ # All other options that relate to the content of the table and how it looks should be configured
13
+ # on the PDF::Wrapper::Table object that is passed into this function.
14
+ #
15
+ # <tt>:left</tt>:: The x co-ordinate of the left-hand side of the table. Defaults to the current cursor location
16
+ # <tt>:top</tt>:: The y co-ordinate of the top of the text. Defaults to the current cursor location
17
+ # <tt>:width</tt>:: The width of the table. Defaults to the distance from the left of the table to the right margin
18
+ def table(data, opts = {})
19
+
20
+ x, y = current_point
21
+ options = {:left => x, :top => y }
22
+ options.merge!(opts)
23
+ options.assert_valid_keys(default_positioning_options.keys)
24
+
25
+ if data.kind_of?(::PDF::Wrapper::Table)
26
+ t = data
27
+ else
28
+ t = ::PDF::Wrapper::Table.new do |table|
29
+ table.data = data
30
+ end
31
+ end
32
+
33
+ t.width = options[:width] || points_to_right_margin(options[:left])
34
+ calc_table_dimensions t
35
+
36
+ # move to the start of our table (the top left)
37
+ move_to(options[:left], options[:top])
38
+
39
+ # draw the header cells
40
+ draw_table_headers(t) if t.headers && (t.show_headers == :page || t.show_headers == :once)
41
+
42
+ x, y = current_point
43
+
44
+ # loop over each row in the table
45
+ t.cells.each_with_index do |row, row_idx|
46
+
47
+ # calc the height of the current row
48
+ h = t.row_height(row_idx)
49
+
50
+ if y + h > absolute_bottom_margin
51
+ start_new_page
52
+ y = margin_top
53
+
54
+ # draw the header cells
55
+ draw_table_headers(t) if t.headers && (t.show_headers == :page)
56
+ x, y = current_point
57
+ end
58
+
59
+ # loop over each column in the current row
60
+ row.each_with_index do |cell, col_idx|
61
+
62
+ # calc the options and widths for this particular cell
63
+ opts = t.options_for(col_idx, row_idx)
64
+ w = t.col_width(col_idx)
65
+
66
+ # paint it
67
+ self.cell(cell.data, x, y, w, h, opts)
68
+ x += w
69
+ move_to(x, y)
70
+ end
71
+
72
+ # move to the start of the next row
73
+ y += h
74
+ x = options[:left]
75
+ move_to(x, y)
76
+ end
77
+ end
78
+
79
+ def calc_table_dimensions(t)
80
+ # TODO: when calculating the min cell width, we basically want the width of the widest character. At the
81
+ # moment I'm stripping all pango markup tags from the string, which means if any character is made
82
+ # intentioanlly large, we'll miss it and it might not fit into our table cell.
83
+ t.cells.each_with_index do |row, row_idx|
84
+ row.each_with_index do |cell, col_idx|
85
+ opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
86
+ padding = opts[:padding] || 3
87
+ cell.min_width = text_width(cell.data.to_s.dup.gsub(/<.+?>/,"").gsub(/\b|\B/,"\n"), opts) + (padding * 4)
88
+ cell.max_width = text_width(cell.data, opts) + (padding * 4)
89
+ end
90
+ end
91
+ if t.headers
92
+ t.headers.each_with_index do |cell, col_idx|
93
+ opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
94
+ padding = opts[:padding] || 3
95
+ cell.min_width = text_width(cell.data.to_s.dup.gsub(/<.+?>/,"").gsub(/\b|\B/,"\n"), opts) + (padding * 4)
96
+ cell.max_width = text_width(cell.data, opts) + (padding * 4)
97
+ end
98
+ end
99
+ t.calc_col_widths!
100
+ t.cells.each_with_index do |row, row_idx|
101
+ row.each_with_index do |cell, col_idx|
102
+ opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
103
+ padding = opts[:padding] || 3
104
+ cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
105
+ end
106
+ end
107
+ t.calc_row_heights!
108
+ if t.headers
109
+ t.headers.each_with_index do |cell, col_idx|
110
+ opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
111
+ padding = opts[:padding] || 3
112
+ cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
113
+ end
114
+ t.calc_headers_height!
115
+ end
116
+ end
117
+ private :calc_table_dimensions
118
+
119
+ def draw_table_headers(t)
120
+ x, y = current_point
121
+ origx = x
122
+ h = t.headers_height
123
+ t.headers.each_with_index do |cell, col_idx|
124
+ # calc the options and widths for this particular header cell
125
+ opts = t.options_for(col_idx, :headers)
126
+ w = t.col_width(col_idx)
127
+
128
+ # paint it
129
+ self.cell(cell.data, x, y, w, h, opts)
130
+ x += w
131
+ move_to(x, y)
132
+ end
133
+ move_to(origx, y + h)
134
+ end
135
+ private :draw_table_headers
136
+
137
+ # This class is used to hold all the data and options for a table that will
138
+ # be added to a PDF::Wrapper document. Tables are a collection of cells, each
139
+ # one rendered to the document using the Wrapper#cell function.
140
+ #
141
+ # To begin working with a table, pass in a 2d array of data to display, along
142
+ # with optional headings, then pass the object to Wrapper#table
143
+ #
144
+ # table = Table.new do |t|
145
+ # t.headings = ["Words", "Numbers"]
146
+ # t.data = [['one', 1],
147
+ # ['two', 2],
148
+ # ['three',3]]
149
+ # end
150
+ # pdf.table(table)
151
+ #
152
+ # For all but the most basic tables, you will probably want to tweak at least
153
+ # some of the options for some of the cells. The options available are the same
154
+ # as those that are valid for the Wrapper#cell method, including things like font,
155
+ # font size, color and alignment.
156
+ #
157
+ # Options can be specified at the table, column, row and cell level. When it comes time
158
+ # to render each cell, the options are merged together so that cell options override row
159
+ # ones, row ones override column ones and column ones override table wide ones.
160
+ #
161
+ # By default, no options are defined at all, and the document defaults will be used.
162
+ #
163
+ # For example:
164
+ #
165
+ # table = Table.new do |t|
166
+ # t.headings = ["Words", "Numbers"]
167
+ # t.data = [['one', 1],
168
+ # ['two', 2],
169
+ # ['three',3]]
170
+ # t.table_options :font_size => 10
171
+ # t.row_options 0, :color => :green
172
+ # t.row_options 2, :color => :red
173
+ # t.col_options 0, :color => :blue
174
+ # t.cell_options 2, 2, :font_size => 18
175
+ # end
176
+ # pdf.table(table)
177
+ #
178
+ # == Displaying Headings
179
+ #
180
+ # By default, the column headings will be displayed at the top of the table, and at
181
+ # the start of each new page the table wraps on to. Use the show_headers= option
182
+ # to change this behaviour. Valid values are nil for never, :once for just the at the
183
+ # top of the table, and :page for the default.
184
+ #
185
+ class Table
186
+ attr_reader :cells, :headers
187
+ attr_accessor :width, :show_headers
188
+
189
+ # Create a new table object.
190
+ #
191
+ # data should be a 2d array
192
+ #
193
+ # [[ "one", "two"],
194
+ # [ "one", "two"]]
195
+ #
196
+ # headers should be a single array
197
+ #
198
+ # ["first", "second"]
199
+ def initialize
200
+
201
+ # default table options
202
+ @table_options = {}
203
+ @col_options = Hash.new({})
204
+ @row_options = Hash.new({})
205
+ @header_options = {}
206
+ @show_headers = :page
207
+
208
+ yield self if block_given?
209
+ self
210
+ end
211
+
212
+ def data=(d)
213
+ # TODO: ensure d is array-like
214
+ @cells = d.collect do |row|
215
+ row.collect do |str|
216
+ Wrapper::Cell.new(str)
217
+ end
218
+ end
219
+ end
220
+
221
+ def headers=(h)
222
+ # TODO: ensure h is array-like
223
+ @headers = h.collect do |str|
224
+ Wrapper::Cell.new(str)
225
+ end
226
+ end
227
+
228
+ # access a particular cell
229
+ def cell(col_idx, row_idx)
230
+ @cells[row_idx, col_idx]
231
+ end
232
+
233
+ # Set or retrieve options that apply to every cell in the table.
234
+ # For a list of valid options, see Wrapper#cell.
235
+ def table_options(opts = nil)
236
+ @table_options = @table_options.merge(opts) if opts
237
+ @table_options
238
+ end
239
+
240
+ # set or retrieve options that apply to header cells
241
+ # For a list of valid options, see Wrapper#cell.
242
+ def header_options(opts = nil)
243
+ @header_options = @header_options.merge(opts) if opts
244
+ @header_options
245
+ end
246
+
247
+ # set or retrieve options that apply to a single cell
248
+ # For a list of valid options, see Wrapper#cell.
249
+ def cell_options(col_idx, row_idx, opts = nil)
250
+ raise ArgumentError, "#{col_idx},#{row_idx} is not a valid cell reference" unless @cells[row_idx] && @cells[row_idx][col_idx]
251
+ @cells[row_idx][col_idx].options = @cells[row_idx][col_idx].options.merge(opts) if opts
252
+ @cells[row_idx][col_idx].options
253
+ end
254
+
255
+ # set options that apply to 1 or more columns
256
+ # For a list of valid options, see Wrapper#cell.
257
+ # <tt>spec</tt>:: Which columns to add the options to. :odd, :even, a range, an Array of numbers or a number
258
+ def col_options(spec, opts)
259
+ each_column do |col_idx|
260
+ if (spec == :even && (col_idx % 2) == 0) ||
261
+ (spec == :odd && (col_idx % 2) == 1) ||
262
+ (spec.class == Range && spec.include?(col_idx)) ||
263
+ (spec.class == Array && spec.include?(col_idx)) ||
264
+ (spec.respond_to?(:to_i) && spec.to_i == col_idx)
265
+
266
+ @col_options[col_idx] = @col_options[col_idx].merge(opts)
267
+ end
268
+ end
269
+ self
270
+ end
271
+
272
+ # set options that apply to 1 or more rows
273
+ # For a list of valid options, see Wrapper#cell.
274
+ # <tt>spec</tt>:: Which columns to add the options to. :odd, :even, a range, an Array of numbers or a number
275
+ def row_options(spec, opts)
276
+ each_row do |row_idx|
277
+ if (spec == :even && (row_idx % 2) == 0) ||
278
+ (spec == :odd && (row_idx % 2) == 1) ||
279
+ (spec.class == Range && spec.include?(row_idx)) ||
280
+ (spec.class == Array && spec.include?(row_idx)) ||
281
+ (spec.respond_to?(:to_i) && spec.to_i == row_idx)
282
+
283
+ @row_options[row_idx] = @col_options[row_idx].merge(opts)
284
+ end
285
+ end
286
+ self
287
+ end
288
+
289
+ # calculate the combined options for a particular cell
290
+ #
291
+ # To get the options for a regular cell, use numbers to refernce the exact cell:
292
+ #
293
+ # options_for(3, 3)
294
+ #
295
+ # To get options for a header cell, use :headers for the row:
296
+ #
297
+ # options_for(3, :headers)
298
+ #
299
+ def options_for(col_idx, row_idx = nil)
300
+ opts = @table_options.dup
301
+ opts.merge! @col_options[col_idx]
302
+ if row_idx == :headers
303
+ opts.merge! @header_options
304
+ else
305
+ opts.merge! @row_options[row_idx]
306
+ opts.merge! @cells[row_idx][col_idx].options
307
+ end
308
+ opts
309
+ end
310
+
311
+ # Returns the required height for the headers row.
312
+ # Essentially just the height of the tallest cell in the row.
313
+ def headers_height
314
+ raise "You must call calc_headers_height! before calling headers_height" if @headers_height.nil?
315
+ @headers_height
316
+ end
317
+
318
+ # Returns the required height for a particular row.
319
+ # Essentially just the height of the tallest cell in the row.
320
+ def row_height(idx)
321
+ raise "You must call calc_row_heights! before calling row_heights" if @row_heights.nil?
322
+ @row_heights[idx]
323
+ end
324
+
325
+ # Returns the number of columns in the table
326
+ def col_count
327
+ @cells.first.size.to_f
328
+ end
329
+
330
+ # Returns the width of the specified column
331
+ def col_width(idx)
332
+ raise "You must call calc_col_widths! before calling col_width" if @col_widths.nil?
333
+ @col_widths[idx]
334
+ end
335
+
336
+ # process the individual cell widths and decide on the resulting
337
+ # width of each column in the table
338
+ def calc_col_widths!
339
+ @col_widths = calc_column_widths
340
+ end
341
+
342
+ # process the individual cell heights in the header and decide on the
343
+ # resulting height of each row in the table
344
+ def calc_headers_height!
345
+ @headers_height = @headers.collect { |cell| cell.height }.compact.max
346
+ end
347
+
348
+ # process the individual cell heights and decide on the resulting
349
+ # height of each row in the table
350
+ def calc_row_heights!
351
+ @row_heights = @cells.collect do |row|
352
+ row.collect { |cell| cell.height }.compact.max
353
+ end
354
+ end
355
+
356
+ # forget row and column dimensions
357
+ def reset!
358
+ @col_widths = nil
359
+ @row_heights = nil
360
+ end
361
+
362
+ private
363
+
364
+ # the main smarts behind deciding on the width of each column. If possible,
365
+ # each cell will get the maximum amount of space it wants. If not, some
366
+ # negotiation happens to find the best possible set of widths.
367
+ def calc_column_widths
368
+ raise "Can't calculate column widths without knowing the overall table width" if self.width.nil?
369
+ check_cell_widths
370
+
371
+ max_col_widths = {}
372
+ min_col_widths = {}
373
+ each_column do |col|
374
+ min_col_widths[col] = cells_in_col(col).collect { |c| c.min_width}.max.to_f
375
+ max_col_widths[col] = cells_in_col(col).collect { |c| c.max_width}.max.to_f
376
+ end
377
+ # add header cells to the mix
378
+ if @headers
379
+ @headers.each_with_index do |cell, idx|
380
+ min_col_widths[idx] = [cell.min_width.to_f, min_col_widths[idx]].max
381
+ max_col_widths[idx] = [cell.max_width.to_f, max_col_widths[idx]].max
382
+ end
383
+ end
384
+
385
+ if min_col_widths.values.sum > self.width
386
+ raise RuntimeError, "table content cannot fit into a table width of #{self.width}"
387
+ end
388
+
389
+ if max_col_widths.values.sum == self.width
390
+ # every col gets the space it wants
391
+ col_widths = max_col_widths.dup
392
+ elsif max_col_widths.values.sum < self.width
393
+ # every col gets the space it wants, and there's
394
+ # still more room left. Distribute the extra room evenly
395
+ col_widths = grow_col_widths(max_col_widths.dup, max_col_widths, true)
396
+ else
397
+ # there's not enough room for every col to get as much space
398
+ # as it wants, so work our way down until it fits
399
+ col_widths = grow_col_widths(min_col_widths.dup, max_col_widths, false)
400
+ end
401
+ col_widths
402
+ end
403
+
404
+ # ceck to ensure every cell has a minimum and maximum cell width defined
405
+ def check_cell_widths
406
+ @cells.each do |row|
407
+ row.each do |cell|
408
+ raise "Every cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
409
+ raise "Every cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
410
+ end
411
+ end
412
+ if @headers
413
+ @headers.each do |cell|
414
+ raise "Every header cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
415
+ raise "Every header cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
416
+ end
417
+ end
418
+ end
419
+
420
+ # iterate over each column in the table
421
+ def each_column(&block)
422
+ (0..(col_count-1)).each do |col|
423
+ yield col
424
+ end
425
+ end
426
+
427
+ # iterate over each row in the table
428
+ def each_row(&block)
429
+ (0..(@cells.size-1)).each do |row|
430
+ yield row
431
+ end
432
+ end
433
+
434
+ # an array of all the cells in the specified column
435
+ def cells_in_col(idx)
436
+ @cells.collect {|row| row[idx]}
437
+ end
438
+
439
+ # an array of all the cells in the specified row
440
+ def cells_in_row(idx)
441
+ @cells[idx]
442
+ end
443
+
444
+ # if the widths of every column are less than the total width
445
+ # of the table, grow them to make use of it.
446
+ #
447
+ # col_widths - the cuurect hash of widths for each column index
448
+ # max_col_widths - the maximum width each column desires
449
+ # pas_max - can the width of a colum grow beyond its maximum desired
450
+ def grow_col_widths(col_widths, max_col_widths, past_max = false)
451
+ loop do
452
+ each_column do |idx|
453
+ col_widths[idx] += 0.3 unless col_widths[idx].frozen?
454
+ col_widths[idx].freeze if col_widths[idx] >= max_col_widths[idx] && past_max == false
455
+ break if col_widths.values.sum >= self.width
456
+ end
457
+ break if col_widths.values.sum >= self.width
458
+ end
459
+ col_widths
460
+ end
461
+ end
462
+
463
+ # A basic container to hold the required information for each cell
464
+ class Cell
465
+ attr_accessor :data, :options, :height, :min_width, :max_width
466
+
467
+ def initialize(str)
468
+ @data = str
469
+ @options = {}
470
+ end
471
+ end
472
+ end
473
+ end