pdf-wrapper 0.0.7 → 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,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