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