pdf-wrapper 0.2.1 → 0.3.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 +5 -0
- data/Rakefile +5 -1
- data/examples/table.rb +1 -0
- data/examples/table_images.rb +41 -0
- data/examples/table_simple.rb +20 -0
- data/lib/pdf/wrapper.rb +2 -0
- data/lib/pdf/wrapper/table.rb +248 -307
- data/lib/pdf/wrapper/text.rb +12 -12
- data/lib/pdf/wrapper/text_cell.rb +53 -0
- data/lib/pdf/wrapper/text_image_cell.rb +68 -0
- data/specs/graphics_spec.rb +8 -8
- data/specs/tables_spec.rb +234 -1
- data/specs/text_spec.rb +2 -2
- data/specs/wrapper_spec.rb +4 -4
- metadata +10 -7
data/CHANGELOG
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
v0.3.0 (15th October 2009)
|
2
|
+
- remove some deprecated functions from Table
|
3
|
+
- added support for table cells with text and images
|
4
|
+
- no API incompatible changes to 0.2.1
|
5
|
+
|
1
6
|
v0.2.1 (18th November 2008)
|
2
7
|
- Small bugfix to prevent unnecesary STDERR output by pango
|
3
8
|
|
data/Rakefile
CHANGED
@@ -5,8 +5,10 @@ require 'rake/rdoctask'
|
|
5
5
|
require 'rake/testtask'
|
6
6
|
require "rake/gempackagetask"
|
7
7
|
require 'spec/rake/spectask'
|
8
|
+
require 'roodi'
|
9
|
+
require 'roodi_task'
|
8
10
|
|
9
|
-
PKG_VERSION = "0.
|
11
|
+
PKG_VERSION = "0.3.0"
|
10
12
|
PKG_NAME = "pdf-wrapper"
|
11
13
|
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
12
14
|
|
@@ -84,3 +86,5 @@ Rake::GemPackageTask.new(spec) do |pkg|
|
|
84
86
|
pkg.need_zip = true
|
85
87
|
pkg.need_tar = true
|
86
88
|
end
|
89
|
+
|
90
|
+
RoodiTask.new 'roodi', ['lib/**/*.rb']
|
data/examples/table.rb
CHANGED
@@ -31,6 +31,7 @@ table = PDF::Wrapper::Table.new(:font_size => 10) do |t|
|
|
31
31
|
t.col_options 3, {:alignment => :centre, :border => "tb"}
|
32
32
|
t.col_options :even, {:fill_color => :blue}
|
33
33
|
t.cell_options 3, 3, {:fill_color => :green}
|
34
|
+
#t.manual_col_width 0, 200
|
34
35
|
end
|
35
36
|
|
36
37
|
pdf.table(table)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new("image_table.pdf", :paper => :A4)
|
9
|
+
pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8.txt").strip, :alignment => :centre
|
10
|
+
pdf.pad 5
|
11
|
+
headers = %w{one two three four}
|
12
|
+
|
13
|
+
image_cell = PDF::Wrapper::TextImageCell.new("9781857233001", File.dirname(__FILE__) + "/../specs/data/orc.svg", 150, 100)
|
14
|
+
|
15
|
+
data = []
|
16
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
17
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
18
|
+
data << [image_cell,2,3,4]
|
19
|
+
|
20
|
+
data << [[], "j", "a", "m"]
|
21
|
+
|
22
|
+
(1..100).each do
|
23
|
+
data << %w{1 2 3 4}
|
24
|
+
end
|
25
|
+
|
26
|
+
table = PDF::Wrapper::Table.new(:font_size => 10) do |t|
|
27
|
+
t.data = data
|
28
|
+
t.headers headers, {:color => :white, :fill_color => :black}
|
29
|
+
t.row_options 6, {:border => "t"}
|
30
|
+
t.row_options :even, {:fill_color => :gray}
|
31
|
+
t.col_options 0, {:border => "tb"}
|
32
|
+
t.col_options 1, {:alignment => :centre}
|
33
|
+
t.col_options 2, {:alignment => :centre}
|
34
|
+
t.col_options 3, {:alignment => :centre, :border => "tb"}
|
35
|
+
t.col_options :even, {:fill_color => :blue}
|
36
|
+
t.cell_options 3, 3, {:fill_color => :green}
|
37
|
+
#t.manual_col_width 0, 200
|
38
|
+
end
|
39
|
+
|
40
|
+
pdf.table(table)
|
41
|
+
pdf.finish
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
5
|
+
|
6
|
+
require 'pdf/wrapper'
|
7
|
+
|
8
|
+
pdf = PDF::Wrapper.new("table.pdf", :paper => :A4)
|
9
|
+
|
10
|
+
data = []
|
11
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
12
|
+
data << ["This is some longer text to ensure that the cell wraps","oh noes! the cols can't get the width they desire",3,4]
|
13
|
+
data << [[], "j", "a", "m"]
|
14
|
+
|
15
|
+
(1..100).each do
|
16
|
+
data << %w{1 2 3 4}
|
17
|
+
end
|
18
|
+
|
19
|
+
pdf.table(data)
|
20
|
+
pdf.finish
|
data/lib/pdf/wrapper.rb
CHANGED
@@ -9,6 +9,8 @@ require 'fileutils'
|
|
9
9
|
require File.dirname(__FILE__) + "/wrapper/graphics"
|
10
10
|
require File.dirname(__FILE__) + "/wrapper/images"
|
11
11
|
require File.dirname(__FILE__) + "/wrapper/loading"
|
12
|
+
require File.dirname(__FILE__) + "/wrapper/text_cell"
|
13
|
+
require File.dirname(__FILE__) + "/wrapper/text_image_cell"
|
12
14
|
require File.dirname(__FILE__) + "/wrapper/table"
|
13
15
|
require File.dirname(__FILE__) + "/wrapper/text"
|
14
16
|
require File.dirname(__FILE__) + "/wrapper/page"
|
data/lib/pdf/wrapper/table.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module PDF
|
2
2
|
class Wrapper
|
3
3
|
|
4
|
-
# Draws a basic table of text on the page. See the documentation for
|
5
|
-
# how to control the table and its appearance.
|
4
|
+
# Draws a basic table of text on the page. See the documentation for PDF::Wrapper::Table to get
|
5
|
+
# a detailed description of how to control the table and its appearance. If data is an array,
|
6
|
+
# it can contain Cell-like objects (see PDF::Wrapper::TextCell and PDF::Wrapper::TextImageCell)
|
7
|
+
# or any objects that respond to to_s().
|
6
8
|
#
|
7
9
|
# <tt>data</tt>:: a 2d array with the data for the columns, or a PDF::Wrapper::Table object
|
8
10
|
#
|
@@ -17,7 +19,7 @@ module PDF
|
|
17
19
|
# <tt>:width</tt>:: The width of the table. Defaults to the distance from the left of the table to the right margin
|
18
20
|
def table(data, opts = {})
|
19
21
|
# TODO: add support for a table footer
|
20
|
-
# - repeating each page, or just at the bottom?
|
22
|
+
# - repeating each page, or just at the bottom?
|
21
23
|
# - if it repeats, should it be different on each page? ie. a sum of that pages rows, etc.
|
22
24
|
# TODO: maybe support for multiple data sets with group headers/footers. useful for subtotals, etc
|
23
25
|
|
@@ -35,137 +37,13 @@ module PDF
|
|
35
37
|
end
|
36
38
|
|
37
39
|
t.width = options[:width] || points_to_right_margin(options[:left])
|
38
|
-
|
39
|
-
|
40
|
-
# move to the start of our table (the top left)
|
41
|
-
move_to(options[:left], options[:top])
|
42
|
-
|
43
|
-
# draw the header cells
|
44
|
-
draw_table_headers(t) if t.headers && (t.show_headers == :page || t.show_headers == :once)
|
45
|
-
|
46
|
-
x, y = current_point
|
47
|
-
|
48
|
-
# loop over each row in the table
|
49
|
-
t.cells.each_with_index do |row, row_idx|
|
50
|
-
|
51
|
-
# calc the height of the current row
|
52
|
-
h = t.row_height(row_idx)
|
53
|
-
|
54
|
-
if y + h > absolute_bottom_margin
|
55
|
-
start_new_page
|
56
|
-
y = margin_top
|
57
|
-
|
58
|
-
# draw the header cells
|
59
|
-
draw_table_headers(t) if t.headers && (t.show_headers == :page)
|
60
|
-
x, y = current_point
|
61
|
-
end
|
62
|
-
|
63
|
-
# loop over each column in the current row
|
64
|
-
row.each_with_index do |cell, col_idx|
|
65
|
-
|
66
|
-
# calc the options and widths for this particular cell
|
67
|
-
opts = t.options_for(col_idx, row_idx)
|
68
|
-
w = t.col_width(col_idx)
|
69
|
-
|
70
|
-
# paint it
|
71
|
-
self.cell(cell.data, x, y, w, h, opts)
|
72
|
-
x += w
|
73
|
-
move_to(x, y)
|
74
|
-
end
|
75
|
-
|
76
|
-
# move to the start of the next row
|
77
|
-
y += h
|
78
|
-
x = options[:left]
|
79
|
-
move_to(x, y)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def calc_table_dimensions(t)
|
84
|
-
# TODO: when calculating the min cell width, we basically want the width of the widest character. At the
|
85
|
-
# moment I'm stripping all pango markup tags from the string, which means if any character is made
|
86
|
-
# intentioanlly large, we'll miss it and it might not fit into our table cell.
|
87
|
-
|
88
|
-
# calculate the min and max width of every cell in the table
|
89
|
-
t.cells.each_with_index do |row, row_idx|
|
90
|
-
row.each_with_index do |cell, col_idx|
|
91
|
-
opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
|
92
|
-
padding = opts[:padding] || 3
|
93
|
-
if opts[:markup] == :pango
|
94
|
-
str = cell.data.to_s.dup.gsub(/<.+?>/,"").gsub("&","&").gsub("<","<").gsub(">",">")
|
95
|
-
opts.delete(:markup)
|
96
|
-
else
|
97
|
-
str = cell.data.to_s.dup
|
98
|
-
end
|
99
|
-
cell.min_width = text_width(str.gsub(/\b|\B/,"\n"), opts) + (padding * 4)
|
100
|
-
cell.max_width = text_width(str, opts) + (padding * 4)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# calculate the min and max width of every cell in the headers row
|
105
|
-
if t.headers
|
106
|
-
t.headers.each_with_index do |cell, col_idx|
|
107
|
-
opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
|
108
|
-
padding = opts[:padding] || 3
|
109
|
-
if opts[:markup] == :pango
|
110
|
-
str = cell.data.to_s.dup.gsub(/<.+?>/,"").gsub("&","&").gsub("<","<").gsub(">",">")
|
111
|
-
opts.delete(:markup)
|
112
|
-
else
|
113
|
-
str = cell.data.to_s.dup
|
114
|
-
end
|
115
|
-
cell.min_width = text_width(str.gsub(/\b|\B/,"\n"), opts) + (padding * 4)
|
116
|
-
cell.max_width = text_width(str, opts) + (padding * 4)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
# let the table decide on the actual widths it will use for each col
|
121
|
-
t.calc_col_widths!
|
122
|
-
|
123
|
-
# now that we know how wide each column will be, we can calculate the
|
124
|
-
# height of every cell in the table
|
125
|
-
t.cells.each_with_index do |row, row_idx|
|
126
|
-
row.each_with_index do |cell, col_idx|
|
127
|
-
opts = t.options_for(col_idx, row_idx).only(default_text_options.keys)
|
128
|
-
padding = opts[:padding] || 3
|
129
|
-
cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# let the table calculate how high each row is going to be
|
134
|
-
t.calc_row_heights!
|
135
|
-
|
136
|
-
# perform the same height calcs for the header row if necesary
|
137
|
-
if t.headers
|
138
|
-
t.headers.each_with_index do |cell, col_idx|
|
139
|
-
opts = t.options_for(col_idx, :headers).only(default_text_options.keys)
|
140
|
-
padding = opts[:padding] || 3
|
141
|
-
cell.height = text_height(cell.data, t.col_width(col_idx) - (padding * 2), opts) + (padding * 2)
|
142
|
-
end
|
143
|
-
t.calc_headers_height!
|
144
|
-
end
|
145
|
-
end
|
146
|
-
private :calc_table_dimensions
|
147
|
-
|
148
|
-
def draw_table_headers(t)
|
149
|
-
x, y = current_point
|
150
|
-
origx = x
|
151
|
-
h = t.headers_height
|
152
|
-
t.headers.each_with_index do |cell, col_idx|
|
153
|
-
# calc the options and widths for this particular header cell
|
154
|
-
opts = t.options_for(col_idx, :headers)
|
155
|
-
w = t.col_width(col_idx)
|
156
|
-
|
157
|
-
# paint it
|
158
|
-
self.cell(cell.data, x, y, w, h, opts)
|
159
|
-
x += w
|
160
|
-
move_to(x, y)
|
161
|
-
end
|
162
|
-
move_to(origx, y + h)
|
40
|
+
t.draw(self, options[:left], options[:top])
|
163
41
|
end
|
164
|
-
private :draw_table_headers
|
165
42
|
|
166
43
|
# This class is used to hold all the data and options for a table that will
|
167
44
|
# be added to a PDF::Wrapper document. Tables are a collection of cells, each
|
168
|
-
# one rendered to the document
|
45
|
+
# one individually rendered to the document in a location that makes it appear
|
46
|
+
# to be a table.
|
169
47
|
#
|
170
48
|
# To begin working with a table, pass in a 2d array of data to display, along
|
171
49
|
# with optional headings, then pass the object to Wrapper#table
|
@@ -178,13 +56,13 @@ module PDF
|
|
178
56
|
# end
|
179
57
|
# pdf.table(table)
|
180
58
|
#
|
181
|
-
# For all but the most basic tables, you will probably want to tweak at least
|
59
|
+
# For all but the most basic tables, you will probably want to tweak at least
|
182
60
|
# some of the options for some of the cells. The options available are the same
|
183
61
|
# as those that are valid for the Wrapper#cell method, including things like font,
|
184
62
|
# font size, color and alignment.
|
185
63
|
#
|
186
64
|
# Options can be specified at the table, column, row and cell level. When it comes time
|
187
|
-
# to render each cell, the options are merged together so that cell options override row
|
65
|
+
# to render each cell, the options are merged together so that cell options override row
|
188
66
|
# ones, row ones override column ones and column ones override table wide ones.
|
189
67
|
#
|
190
68
|
# By default, no options are defined at all, and the document defaults will be used.
|
@@ -211,39 +89,73 @@ module PDF
|
|
211
89
|
# to change this behaviour. Valid values are nil for never, :once for just the at the
|
212
90
|
# top of the table, and :page for the default.
|
213
91
|
#
|
92
|
+
# == Complex Cells
|
93
|
+
#
|
94
|
+
# By default, any cell content described in the data array is converted to a string and
|
95
|
+
# wrapped in a TextCell object. If you need to, it is possible to define your cells
|
96
|
+
# as cell-like objects manually to get more control.
|
97
|
+
#
|
98
|
+
# The following two calls are equivilant:
|
99
|
+
#
|
100
|
+
# data = [[1,2]]
|
101
|
+
# pdf.table(data)
|
102
|
+
#
|
103
|
+
# data = [[PDF::Wrapper::TextCell.new(2),PDF::Wrapper::TextCell.new(2)]]
|
104
|
+
# pdf.table(data)
|
105
|
+
#
|
106
|
+
# An alternative to a text-only cell is a cell with text and an image. These
|
107
|
+
# cells must be initialised with a filename and cell dimensions (width and height)
|
108
|
+
# as calculating automatic dimensions is difficult.
|
109
|
+
#
|
110
|
+
# data = [
|
111
|
+
# ["James", PDF::Wrapper::TextImageCell.new("Healy","photo-jim.jpg",100,100)],
|
112
|
+
# ["Jess", PDF::Wrapper::TextImageCell.new("Healy","photo-jess.jpg",100,100)],
|
113
|
+
# ]
|
114
|
+
# pdf.table(data)
|
115
|
+
#
|
116
|
+
# If TextImageCell doesn't meet your needs, you are free to define your own
|
117
|
+
# cell-like object and use that.
|
118
|
+
#
|
214
119
|
class Table
|
215
|
-
attr_reader :cells
|
120
|
+
attr_reader :cells, :wrapper
|
216
121
|
attr_accessor :width, :show_headers
|
217
122
|
|
218
|
-
#
|
219
123
|
def initialize(opts = {})
|
220
124
|
|
221
125
|
# default table options
|
222
126
|
@table_options = opts
|
223
|
-
@col_options = Hash.new({})
|
224
|
-
@row_options = Hash.new({})
|
225
127
|
@manual_col_widths = {}
|
226
|
-
@header_options = {}
|
227
128
|
@show_headers = :page
|
228
129
|
|
229
130
|
yield self if block_given?
|
230
131
|
self
|
231
132
|
end
|
232
133
|
|
233
|
-
#
|
134
|
+
# Set the table data.
|
234
135
|
#
|
235
136
|
# The single argument should be a 2d array like:
|
236
137
|
#
|
237
138
|
# [[ "one", "two"],
|
238
139
|
# [ "one", "two"]]
|
140
|
+
#
|
141
|
+
# The cells in the array can be any object with to_s() defined, or a Cell-like
|
142
|
+
# object (such as a TextCell or TextImageCell).
|
143
|
+
#
|
239
144
|
def data=(d)
|
240
|
-
|
241
|
-
|
145
|
+
row_sizes = d.map { |row| row.size }.compact.uniq
|
146
|
+
raise ArgumentError, "" if row_sizes.size > 1
|
147
|
+
|
242
148
|
@cells = d.collect do |row|
|
243
|
-
row.collect do |
|
244
|
-
Wrapper::
|
149
|
+
row.collect do |data|
|
150
|
+
if data.kind_of?(Wrapper::TextCell) || data.kind_of?(Wrapper::TextImageCell)
|
151
|
+
data
|
152
|
+
else
|
153
|
+
Wrapper::TextCell.new(data.to_s)
|
154
|
+
end
|
245
155
|
end
|
246
156
|
end
|
157
|
+
each_cell { |cell| cell.options.merge!(@table_options)}
|
158
|
+
@cells
|
247
159
|
end
|
248
160
|
|
249
161
|
# Retrieve or set the table's optional column headers.
|
@@ -258,57 +170,78 @@ module PDF
|
|
258
170
|
# t.headers ["col one", "col two]
|
259
171
|
#
|
260
172
|
# The optional second argument sets the cell options for the header
|
261
|
-
# cells. See PDF::Wrapper#cell for a list of possible options.
|
173
|
+
# cells. See PDF::Wrapper#cell for a list of possible options.
|
262
174
|
#
|
263
175
|
# t.headers ["col one", "col two], :color => :block, :fill_color => :black
|
264
176
|
#
|
265
|
-
# If the options hash is left unspecified, the default table options will
|
177
|
+
# If the options hash is left unspecified, the default table options will
|
266
178
|
# be used.
|
267
179
|
#
|
268
180
|
def headers(h = nil, opts = {})
|
269
|
-
# TODO: raise an exception of the size of the array does not match the size
|
270
|
-
# of the data row arrays
|
271
|
-
# TODO: ensure h is array-like
|
272
181
|
return @headers if h.nil?
|
273
|
-
|
274
|
-
|
182
|
+
|
183
|
+
if @cells && @cells.first.size != h.size
|
184
|
+
raise ArgumentError, "header column count does not match data column count"
|
275
185
|
end
|
276
|
-
@header_options = opts
|
277
|
-
end
|
278
186
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
187
|
+
@headers = h.collect do |data|
|
188
|
+
if data.kind_of?(Wrapper::TextCell) || data.kind_of?(Wrapper::TextImageCell)
|
189
|
+
data
|
190
|
+
else
|
191
|
+
Wrapper::TextCell.new(data.to_s)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
@headers.each { |cell| cell.options.merge!(@table_options)}
|
195
|
+
@headers.each { |cell| cell.options.merge!(opts)}
|
196
|
+
@headers
|
283
197
|
end
|
284
198
|
|
285
|
-
|
286
|
-
|
287
|
-
@cells[row_idx, col_idx]
|
288
|
-
end
|
199
|
+
def draw(wrapper, tablex, tabley)
|
200
|
+
@wrapper = wrapper
|
289
201
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
202
|
+
calculate_dimensions
|
203
|
+
|
204
|
+
# move to the start of our table (the top left)
|
205
|
+
wrapper.move_to(tablex, tabley)
|
206
|
+
|
207
|
+
# draw the header cells
|
208
|
+
draw_table_headers if self.headers && (self.show_headers == :page || self.show_headers == :once)
|
209
|
+
|
210
|
+
x, y = wrapper.current_point
|
211
|
+
|
212
|
+
# loop over each row in the table
|
213
|
+
self.cells.each_with_index do |row, row_idx|
|
214
|
+
|
215
|
+
# calc the height of the current row
|
216
|
+
h = row.first.height
|
217
|
+
|
218
|
+
if y + h > wrapper.absolute_bottom_margin
|
219
|
+
wrapper.start_new_page
|
220
|
+
y = wrapper.margin_top
|
221
|
+
|
222
|
+
# draw the header cells
|
223
|
+
draw_table_headers if self.headers && (self.show_headers == :page)
|
224
|
+
x, y = wrapper.current_point
|
225
|
+
end
|
226
|
+
|
227
|
+
# loop over each column in the current row and paint it
|
228
|
+
row.each_with_index do |cell, col_idx|
|
229
|
+
cell.draw(wrapper, x, y)
|
230
|
+
x += cell.width
|
231
|
+
wrapper.move_to(x, y)
|
232
|
+
end
|
233
|
+
|
234
|
+
# move to the start of the next row
|
235
|
+
y += h
|
236
|
+
x = tablex
|
237
|
+
wrapper.move_to(x, y)
|
238
|
+
end
|
300
239
|
end
|
301
240
|
|
302
|
-
#
|
303
|
-
# For a list of valid options, see Wrapper#cell.
|
241
|
+
# access a particular cell at location x, y
|
304
242
|
#
|
305
|
-
|
306
|
-
|
307
|
-
def header_options(opts = nil)
|
308
|
-
# TODO: remove this method at some point. Deprecation started on 10th August 2008.
|
309
|
-
warn "WARNING: Table#header_options() is deprecated, please see the documentation for PDF::Wrapper::Table"
|
310
|
-
@header_options = @header_options.merge(opts) if opts
|
311
|
-
@header_options
|
243
|
+
def cell(col_idx, row_idx)
|
244
|
+
@cells[row_idx][col_idx]
|
312
245
|
end
|
313
246
|
|
314
247
|
# set or retrieve options that apply to a single cell
|
@@ -329,14 +262,16 @@ module PDF
|
|
329
262
|
(spec.class == Range && spec.include?(col_idx)) ||
|
330
263
|
(spec.class == Array && spec.include?(col_idx)) ||
|
331
264
|
(spec.respond_to?(:to_i) && spec.to_i == col_idx)
|
332
|
-
|
333
|
-
|
265
|
+
|
266
|
+
cells_in_col(col_idx).each do |cell|
|
267
|
+
cell.options.merge!(opts)
|
268
|
+
end
|
334
269
|
end
|
335
270
|
end
|
336
271
|
self
|
337
272
|
end
|
338
273
|
|
339
|
-
# Manually set the width for 1 or more columns
|
274
|
+
# Manually set the width for 1 or more columns
|
340
275
|
#
|
341
276
|
# <tt>spec</tt>:: Which columns to set the width for. :odd, :even, a range, an Array of numbers or a number
|
342
277
|
#
|
@@ -348,7 +283,7 @@ module PDF
|
|
348
283
|
(spec.class == Range && spec.include?(col_idx)) ||
|
349
284
|
(spec.class == Array && spec.include?(col_idx)) ||
|
350
285
|
(spec.respond_to?(:to_i) && spec.to_i == col_idx)
|
351
|
-
|
286
|
+
|
352
287
|
@manual_col_widths[col_idx] = width
|
353
288
|
end
|
354
289
|
end
|
@@ -365,174 +300,172 @@ module PDF
|
|
365
300
|
(spec.class == Range && spec.include?(row_idx)) ||
|
366
301
|
(spec.class == Array && spec.include?(row_idx)) ||
|
367
302
|
(spec.respond_to?(:to_i) && spec.to_i == row_idx)
|
368
|
-
|
369
|
-
|
303
|
+
|
304
|
+
cells_in_row(row_idx).each do |cell|
|
305
|
+
cell.options.merge!(opts)
|
306
|
+
end
|
370
307
|
end
|
371
308
|
end
|
372
309
|
self
|
373
310
|
end
|
374
311
|
|
375
|
-
#
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
#
|
381
|
-
# To get options for a header cell, use :headers for the row:
|
382
|
-
#
|
383
|
-
# options_for(3, :headers)
|
312
|
+
# Returns the number of columns in the table
|
313
|
+
def col_count
|
314
|
+
@cells.first.size.to_f
|
315
|
+
end
|
316
|
+
|
317
|
+
# iterate over each cell in the table. Yields a cell object.
|
384
318
|
#
|
385
|
-
def
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
else
|
391
|
-
opts.merge! @row_options[row_idx]
|
392
|
-
opts.merge! @cells[row_idx][col_idx].options
|
319
|
+
def each_cell(&block)
|
320
|
+
each_row do |row_idx|
|
321
|
+
cells_in_row(row_idx).each do |cell|
|
322
|
+
yield cell
|
323
|
+
end
|
393
324
|
end
|
394
|
-
opts
|
395
325
|
end
|
396
326
|
|
397
|
-
|
398
|
-
# Essentially just the height of the tallest cell in the row.
|
399
|
-
def headers_height
|
400
|
-
raise "You must call calc_headers_height! before calling headers_height" if @headers_height.nil?
|
401
|
-
@headers_height
|
402
|
-
end
|
327
|
+
private
|
403
328
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
329
|
+
def draw_table_headers
|
330
|
+
x, y = wrapper.current_point
|
331
|
+
origx = x
|
332
|
+
h = self.headers.first.height
|
333
|
+
self.headers.each_with_index do |cell, col_idx|
|
334
|
+
cell.draw(wrapper, x, y)
|
335
|
+
x += cell.width
|
336
|
+
wrapper.move_to(x, y)
|
337
|
+
end
|
338
|
+
wrapper.move_to(origx, y + h)
|
409
339
|
end
|
410
340
|
|
411
|
-
#
|
412
|
-
|
413
|
-
|
341
|
+
# calculate the dimensions of each row and column in the table. The order
|
342
|
+
# here is crucial. First we ask each cell to caclulate the range of
|
343
|
+
# widths they can render with, then we make a decision on the actual column
|
344
|
+
# width and pass that on to every cell.
|
345
|
+
#
|
346
|
+
# Once each cell knows how wide it will be it can calculate how high it
|
347
|
+
# will be. With that done the table cen determine the tallest cell in
|
348
|
+
# each row and pass that onto each cell so every cell in a row renders
|
349
|
+
# with the same height.
|
350
|
+
#
|
351
|
+
def calculate_dimensions
|
352
|
+
calculate_cell_width_range
|
353
|
+
calculate_column_widths
|
354
|
+
calculate_cell_heights
|
355
|
+
calculate_row_heights
|
414
356
|
end
|
415
357
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
end
|
358
|
+
def calculate_cell_width_range
|
359
|
+
# TODO: when calculating the min cell width, we basically want the width of the widest character. At the
|
360
|
+
# moment I'm stripping all pango markup tags from the string, which means if any character is made
|
361
|
+
# intentioanlly large, we'll miss it and it might not fit into our table cell.
|
421
362
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
363
|
+
# calculate the min and max width of every cell in the table
|
364
|
+
cells.each_with_index do |row, row_idx|
|
365
|
+
row.each_with_index do |cell, col_idx|
|
366
|
+
cell.calculate_width_range(wrapper)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
# calculate the min and max width of every cell in the headers row
|
371
|
+
if self.headers
|
372
|
+
self.headers.each_with_index do |cell, col_idx|
|
373
|
+
cell.calculate_width_range(wrapper)
|
374
|
+
end
|
375
|
+
end
|
426
376
|
end
|
427
377
|
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
378
|
+
def calculate_cell_heights
|
379
|
+
cells.each_with_index do |row, row_idx|
|
380
|
+
row.each_with_index do |cell, col_idx|
|
381
|
+
cell.calculate_height(wrapper)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# perform the same height calcs for the header row if necesary
|
386
|
+
if self.headers
|
387
|
+
self.headers.each_with_index do |cell, col_idx|
|
388
|
+
cell.calculate_height(wrapper)
|
389
|
+
end
|
390
|
+
end
|
432
391
|
end
|
433
392
|
|
434
393
|
# process the individual cell heights and decide on the resulting
|
435
394
|
# height of each row in the table
|
436
|
-
def
|
437
|
-
@
|
438
|
-
row.collect { |cell| cell.height }.compact.max
|
395
|
+
def calculate_row_heights
|
396
|
+
@cells.each do |row|
|
397
|
+
row_height = row.collect { |cell| cell.height }.compact.max
|
398
|
+
row.each { |cell| cell.height = row_height }
|
439
399
|
end
|
440
|
-
end
|
441
400
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
401
|
+
if @headers
|
402
|
+
row_height = @headers.collect { |cell| cell.height }.compact.max
|
403
|
+
self.headers.each_with_index do |cell, col_idx|
|
404
|
+
cell.height = row_height
|
405
|
+
end
|
406
|
+
end
|
446
407
|
end
|
447
408
|
|
448
|
-
private
|
449
|
-
|
450
409
|
# the main smarts behind deciding on the width of each column. If possible,
|
451
410
|
# each cell will get the maximum amount of space it wants. If not, some
|
452
411
|
# negotiation happens to find the best possible set of widths.
|
453
|
-
|
412
|
+
#
|
413
|
+
def calculate_column_widths
|
454
414
|
raise "Can't calculate column widths without knowing the overall table width" if self.width.nil?
|
455
|
-
check_cell_widths
|
456
415
|
|
457
|
-
max_col_widths = {}
|
458
416
|
min_col_widths = {}
|
417
|
+
natural_col_widths = {}
|
418
|
+
max_col_widths = {}
|
459
419
|
each_column do |col|
|
460
|
-
min_col_widths[col] = cells_in_col(col).collect { |c| c.min_width}.max
|
461
|
-
|
462
|
-
|
463
|
-
# add header cells to the mix
|
464
|
-
if @headers
|
465
|
-
@headers.each_with_index do |cell, idx|
|
466
|
-
min_col_widths[idx] = [cell.min_width.to_f, min_col_widths[idx]].max
|
467
|
-
max_col_widths[idx] = [cell.max_width.to_f, max_col_widths[idx]].max
|
468
|
-
end
|
420
|
+
min_col_widths[col] = cells_in_col(col).collect { |c| c.min_width}.max
|
421
|
+
natural_col_widths[col] = cells_in_col(col).collect { |c| c.natural_width}.max
|
422
|
+
max_col_widths[col] = cells_in_col(col).collect { |c| c.max_width}.compact.max
|
469
423
|
end
|
470
424
|
|
471
425
|
# override the min and max col widths with manual ones where appropriate
|
472
|
-
# freeze the values so that the algorithm that adjusts the widths
|
473
|
-
# leaves them untouched
|
474
|
-
@manual_col_widths.each { |key, val| val.freeze }
|
475
426
|
max_col_widths.merge! @manual_col_widths
|
427
|
+
natural_col_widths.merge! @manual_col_widths
|
476
428
|
min_col_widths.merge! @manual_col_widths
|
477
429
|
|
478
430
|
if min_col_widths.values.sum > self.width
|
479
431
|
raise RuntimeError, "table content cannot fit into a table width of #{self.width}"
|
480
|
-
end
|
481
|
-
|
482
|
-
if max_col_widths.values.sum == self.width
|
483
|
-
# every col gets the space it wants
|
484
|
-
col_widths = max_col_widths.dup
|
485
|
-
elsif max_col_widths.values.sum < self.width
|
486
|
-
# every col gets the space it wants, and there's
|
487
|
-
# still more room left. Distribute the extra room evenly
|
488
|
-
col_widths = grow_col_widths(max_col_widths.dup, max_col_widths, true)
|
489
432
|
else
|
490
433
|
# there's not enough room for every col to get as much space
|
491
434
|
# as it wants, so work our way down until it fits
|
492
|
-
col_widths = grow_col_widths(min_col_widths.dup,
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
# check to ensure every cell has a minimum and maximum cell width defined
|
498
|
-
def check_cell_widths
|
499
|
-
@cells.each do |row|
|
500
|
-
row.each_with_index do |cell, col_idx|
|
501
|
-
raise "Every cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
|
502
|
-
raise "Every cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
|
503
|
-
if @manual_col_widths[col_idx] && cell.min_width > @manual_col_widths[col_idx]
|
504
|
-
raise "Manual width for col #{col_idx} is too low"
|
505
|
-
end
|
506
|
-
end
|
507
|
-
end
|
508
|
-
if @headers
|
509
|
-
@headers.each_with_index do |cell, col_idx|
|
510
|
-
raise "Every header cell must have a min_width defined before being rendered to a document" if cell.min_width.nil?
|
511
|
-
raise "Every header cell must have a max_width defined before being rendered to a document" if cell.max_width.nil?
|
512
|
-
if @manual_col_widths[col_idx] && cell.min_width > @manual_col_widths[col_idx]
|
513
|
-
raise "Manual width for col #{col_idx} is too low"
|
435
|
+
col_widths = grow_col_widths(min_col_widths.dup, natural_col_widths, max_col_widths)
|
436
|
+
col_widths.each do |col_index, width|
|
437
|
+
cells_in_col(col_index).each do |cell|
|
438
|
+
cell.width = width
|
514
439
|
end
|
515
440
|
end
|
516
441
|
end
|
517
442
|
end
|
518
443
|
|
519
|
-
# iterate over each column in the table
|
444
|
+
# iterate over each column in the table. Yields a column index, not
|
445
|
+
# actual columns or cells.
|
446
|
+
#
|
520
447
|
def each_column(&block)
|
521
448
|
(0..(col_count-1)).each do |col|
|
522
449
|
yield col
|
523
450
|
end
|
524
451
|
end
|
525
452
|
|
526
|
-
# iterate over each row in the table
|
453
|
+
# iterate over each row in the table. Yields an row index, not actual rows
|
454
|
+
# or cells.
|
455
|
+
#
|
527
456
|
def each_row(&block)
|
528
457
|
(0..(@cells.size-1)).each do |row|
|
529
458
|
yield row
|
530
459
|
end
|
531
460
|
end
|
532
461
|
|
533
|
-
# an array of all the cells in the specified column
|
462
|
+
# an array of all the cells in the specified column, including headers
|
463
|
+
#
|
534
464
|
def cells_in_col(idx)
|
535
|
-
|
465
|
+
ret = []
|
466
|
+
ret << @headers[idx] if @headers
|
467
|
+
ret += @cells.collect {|row| row[idx]}
|
468
|
+
ret
|
536
469
|
end
|
537
470
|
|
538
471
|
# an array of all the cells in the specified row
|
@@ -540,17 +473,35 @@ module PDF
|
|
540
473
|
@cells[idx]
|
541
474
|
end
|
542
475
|
|
543
|
-
#
|
544
|
-
#
|
476
|
+
# starting with very low widths for each col, bump each column width up
|
477
|
+
# until we reach the width of the entire table.
|
478
|
+
#
|
479
|
+
# columns that are less than their "natural width" are given preference.
|
480
|
+
# If every column has reached its natural width then each column is
|
481
|
+
# increased in an equal manor.
|
545
482
|
#
|
546
|
-
# col_widths
|
547
|
-
#
|
548
|
-
#
|
549
|
-
|
483
|
+
# starting col_widths
|
484
|
+
# the hash of column widths to start from. Should generally match the
|
485
|
+
# absolute smallest width each column can render in
|
486
|
+
# natural_col_widths
|
487
|
+
# the hqash of column widths where each column will be able to render
|
488
|
+
# itself fully without wrapping
|
489
|
+
# max_col_widths
|
490
|
+
# the hash of absolute maximum column widths, no column width can go
|
491
|
+
# past this. Can be nil, which indicates there's no maximum
|
492
|
+
#
|
493
|
+
def grow_col_widths(starting_col_widths, natural_col_widths, max_col_widths)
|
494
|
+
col_widths = starting_col_widths.dup
|
550
495
|
loop do
|
551
496
|
each_column do |idx|
|
552
|
-
col_widths
|
553
|
-
|
497
|
+
if col_widths.values.sum >= natural_col_widths.values.sum ||
|
498
|
+
col_widths[idx] < natural_col_widths[idx]
|
499
|
+
if max_col_widths[idx].nil? || col_widths[idx] < max_col_widths[idx]
|
500
|
+
col_widths[idx] += 0.3
|
501
|
+
else
|
502
|
+
col_widths[idx] = max_col_widths[idx]
|
503
|
+
end
|
504
|
+
end
|
554
505
|
break if col_widths.values.sum >= self.width
|
555
506
|
end
|
556
507
|
break if col_widths.values.sum >= self.width
|
@@ -558,15 +509,5 @@ module PDF
|
|
558
509
|
col_widths
|
559
510
|
end
|
560
511
|
end
|
561
|
-
|
562
|
-
# A basic container to hold the required information for each cell
|
563
|
-
class Cell
|
564
|
-
attr_accessor :data, :options, :height, :min_width, :max_width
|
565
|
-
|
566
|
-
def initialize(str)
|
567
|
-
@data = str
|
568
|
-
@options = {}
|
569
|
-
end
|
570
|
-
end
|
571
512
|
end
|
572
513
|
end
|