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.
- data/CHANGELOG +11 -0
- data/Rakefile +4 -4
- data/examples/cell.rb +1 -1
- data/examples/image.rb +3 -3
- data/examples/markup.rb +12 -0
- data/examples/repeating.rb +1 -1
- data/examples/scaled.rb +38 -0
- data/examples/scaled_cells.rb +30 -0
- data/examples/scaled_image.rb +14 -0
- data/examples/shapes.rb +1 -1
- data/examples/table.rb +25 -4
- data/examples/translate.rb +21 -0
- data/examples/utf8-long.rb +1 -2
- data/examples/utf8.rb +1 -2
- data/lib/pdf/core.rb +23 -0
- data/lib/pdf/wrapper.rb +136 -760
- data/lib/pdf/wrapper/graphics.rb +116 -0
- data/lib/pdf/wrapper/images.rb +206 -0
- data/lib/pdf/wrapper/loading.rb +52 -0
- data/lib/pdf/wrapper/table.rb +473 -0
- data/lib/pdf/wrapper/text.rb +337 -0
- data/specs/data/windmill.jpg +0 -0
- data/specs/{shapes_spec.rb → graphics_spec.rb} +3 -3
- data/specs/spec_helper.rb +6 -1
- data/specs/tables_spec.rb +111 -0
- data/specs/text_spec.rb +63 -2
- data/specs/wrapper_spec.rb +145 -72
- metadata +20 -9
- data/examples/padded_image.rb +0 -12
- data/examples/template.rb +0 -13
@@ -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
|