rabidprawns 0.0.1
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/README.rdoc +79 -0
- data/lib/rabidprawns/columned_page.rb +74 -0
- data/lib/rabidprawns/simple_tables.rb +476 -0
- data/lib/rabidprawns.rb +38 -0
- data/rabidprawns.gemspec +13 -0
- metadata +71 -0
data/README.rdoc
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
== RabidPrawns - Document Helpers
|
2
|
+
|
3
|
+
RabidPrawns is a simple library that loads some helpers into Prawn::Document for page layouts. Currently
|
4
|
+
two "rabids" are available to load into Prawn::Document. These helper methods should work on most versions
|
5
|
+
of Prawn and have been somewhat tested on versions starting at 0.6.3 through 0.8.4. If you find any bugs,
|
6
|
+
please create an issue on the github site.
|
7
|
+
|
8
|
+
== Loading Rabids
|
9
|
+
|
10
|
+
Loading rabids into the prawn document is easy:
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'rabidprawns'
|
14
|
+
|
15
|
+
RabidPrawns.load_rabid :columned_page
|
16
|
+
|
17
|
+
That's it. Your next Prawn::Document instance will have a Prawn::Document#columned_page method for writing out columned pages.
|
18
|
+
|
19
|
+
== Available Rabids
|
20
|
+
|
21
|
+
Currently, there are only two rabids available. To get a list of available rabids:
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'rabidprawns'
|
25
|
+
|
26
|
+
puts RabidPrawns.available_rabids
|
27
|
+
|
28
|
+
=== Columned Page
|
29
|
+
|
30
|
+
The columned page rabid will provide a Prawn::Document#columned_page method. It takes the text for the pages, the title to use
|
31
|
+
for the pages and some options for how things should be laid out. If the content fills the page, it will automatically
|
32
|
+
move to the next page, reprint the title and continue printing the content. Here's a simple example script you
|
33
|
+
can run:
|
34
|
+
|
35
|
+
require 'rubygems'
|
36
|
+
require 'rabidprawns'
|
37
|
+
|
38
|
+
RabidPrawns.load_rabid :columned_page
|
39
|
+
doc = Prawn::Document.new
|
40
|
+
doc.columned_page('this is some text to print ' * 1000, 'This is the title', :columns => 3, :title_colspan => 2)
|
41
|
+
output = File.open('example.pdf', 'w+')
|
42
|
+
output.write doc.render
|
43
|
+
output.close
|
44
|
+
puts 'Example complete'
|
45
|
+
|
46
|
+
There are a few options available. Take a look at the docs for a complete listing.
|
47
|
+
|
48
|
+
=== Simple Tables
|
49
|
+
|
50
|
+
This rabid will provide a Prawn::Document#simple_tables method for printing tables. The idea is simple. Provide an array of table structures
|
51
|
+
in an array, with any desired options, and print the tables on the page. Simple tables will properly break at the end of a page,
|
52
|
+
accounting for margin restrictions, and start a new page, with table title and headings. Here is a simple example:
|
53
|
+
|
54
|
+
require 'rubygems'
|
55
|
+
require 'rabidprawns'
|
56
|
+
|
57
|
+
RabidPrawns.load_rabid :simple_tables
|
58
|
+
|
59
|
+
a = Prawn::Document.new
|
60
|
+
tables = [
|
61
|
+
{
|
62
|
+
:title => 'thing',
|
63
|
+
:contents=> [
|
64
|
+
{
|
65
|
+
:headings => ["one", "two", "three", "four"],
|
66
|
+
:rows=>[[1, 2, 3, 4], [{:color => [100, 0, 0, 0], :background_color => [0, 100, 0, 0], :content => "this"}, "is", "working", "hopefully"], [nil, 'a', 'b', 'c']]
|
67
|
+
}
|
68
|
+
]
|
69
|
+
}
|
70
|
+
]
|
71
|
+
a.simple_tables tables
|
72
|
+
f = File.open('example.pdf', 'w+')
|
73
|
+
f.write a.render
|
74
|
+
f.close
|
75
|
+
puts 'Example complete'
|
76
|
+
|
77
|
+
Simple tables has a few nice features, like dynamically changing column numbers within a table (this is why :contents is an Array), applying scoped options at various
|
78
|
+
points during table construction, automatically sizing the table to fit within constraints, printing title and/or headings on new pages and much more. For a longer
|
79
|
+
explanation of features, and available options, take a look at the RDoc for the #simple_tables method.
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Prawn
|
2
|
+
class Document
|
3
|
+
# text:: Text to print
|
4
|
+
# title:: Title of page
|
5
|
+
# options:: Options hash
|
6
|
+
# Print a columned page
|
7
|
+
# Available options:
|
8
|
+
# :columns -> number of columns on page
|
9
|
+
# :title_colspan -> number of columns title should span
|
10
|
+
# :column_pad -> padding between columns
|
11
|
+
# :title_font -> Array of font information
|
12
|
+
# :default_font -> Array of font information
|
13
|
+
# :new_page -> lambda to be called to start a new page
|
14
|
+
# :finalize_page -> lambda to be called when a page has been completed
|
15
|
+
# :page_height -> height of the page
|
16
|
+
# :page_width -> width of the page
|
17
|
+
# :margin -> margin on all sides
|
18
|
+
# NOTE: Below margin options will have :margin added to them
|
19
|
+
# :margin_top -> margin on top
|
20
|
+
# :margin_bottom -> margin on bottom
|
21
|
+
# :margin_left -> margin on left
|
22
|
+
# :margin_right -> margin on right
|
23
|
+
# :title_to_text_pad -> padding between title and start of text
|
24
|
+
# :new_page_at_start -> move to new page before starting
|
25
|
+
def columned_page(text, title, options = {})
|
26
|
+
opts = {:columns => 2, :title_colspan => 1, :column_pad => 10,
|
27
|
+
:title_font => [font_families.keys.first, {:style => font_families.values.first.keys.first, :size => 18}],
|
28
|
+
:default_font => [font_families.keys.first, {:style => font_families.values.first.keys.first, :size => 6}],
|
29
|
+
:new_page => lambda{ start_new_page }, :finalize_page => lambda{},
|
30
|
+
:page_height => bounds.height, :page_width => bounds.width, :margin => 0,
|
31
|
+
:margin_top => 0, :margin_bottom => 0, :margin_left => 0, :margin_right => 0,
|
32
|
+
:title_to_text_pad => 5, :new_page_at_start => false
|
33
|
+
}.merge(options)
|
34
|
+
[:columns, :title_colspan, :column_pad, :page_height, :page_width,
|
35
|
+
:margin, :margin_top, :margin_bottom, :margin_left, :margin_right].each{|x| opts[x] = opts[x].to_i}
|
36
|
+
opts[:title_colspan] = opts[:columns] if opts[:title_colspan] > opts[:columns]
|
37
|
+
[:margin_top, :margin_bottom, :margin_left, :margin_right].each{|x|opts[x] += opts[:margin]} if opts[:margin] > 0
|
38
|
+
if(opts[:new_page_at_start])
|
39
|
+
opts[:new_page].call
|
40
|
+
else
|
41
|
+
opts[:new_page_at_start] = true
|
42
|
+
end
|
43
|
+
ypos = opts[:page_height] - opts[:margin_top]
|
44
|
+
columns = []
|
45
|
+
total_width = opts[:page_width] - (opts[:column_pad] * opts[:columns]) - opts[:margin_left] - opts[:margin_right]
|
46
|
+
column_width = total_width.to_f / opts[:columns]
|
47
|
+
title_height = 0
|
48
|
+
font(*opts[:title_font]) do
|
49
|
+
title_width = (column_width * opts[:title_colspan]) + (opts[:column_pad] * (opts[:title_colspan] - 1))
|
50
|
+
title_height = height_of(title, title_width)
|
51
|
+
bounding_box([opts[:margin_left], ypos], :width => title_width, :height => title_height) do
|
52
|
+
text title
|
53
|
+
end
|
54
|
+
end
|
55
|
+
font(*opts[:default_font]) do
|
56
|
+
ypos -= title_height + opts[:title_to_text_pad] # pad
|
57
|
+
below_title = ypos
|
58
|
+
opts[:columns].times do |i|
|
59
|
+
if(i < opts[:title_colspan])
|
60
|
+
ypos = below_title
|
61
|
+
else
|
62
|
+
ypos = opts[:page_height] - opts[:margin_top]
|
63
|
+
end
|
64
|
+
xpos = opts[:margin_left] + (column_width * i) + (opts[:column_pad] * i)
|
65
|
+
bounding_box([xpos, ypos], :width => column_width, :height => opts[:page_height] - (opts[:page_height] - ypos) - opts[:margin_bottom]) do
|
66
|
+
text = text_box text, :height => bounds.height, :width => bounds.width
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
opts[:finalize_page].call
|
71
|
+
columned_page(text, title, options) unless text.empty?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,476 @@
|
|
1
|
+
unless([].respond_to?(:sum))
|
2
|
+
class Array
|
3
|
+
def sum
|
4
|
+
self.inject(:+)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Prawn
|
10
|
+
class Document
|
11
|
+
# tables:: Array of tables
|
12
|
+
# options:: Table options
|
13
|
+
#
|
14
|
+
# Prints tables to document.
|
15
|
+
#
|
16
|
+
# tables array: Each element within this array is a single table, allowing multiple tables to be
|
17
|
+
# printed at once. The format of each element in the array is as follows:
|
18
|
+
#
|
19
|
+
# {:title => "I'm the title of the table",
|
20
|
+
# :contents => [{:headings => ['col1 heading', 'col2 heading', 'col3 heading'],
|
21
|
+
# :rows => [['col1 value', 'col2 value', 'col3 value'],
|
22
|
+
# ['col1 value2', 'col2 value2, {:content => 'col3 value3', :color => [100,0,0,0]}]]
|
23
|
+
# :options => {:headings_text_color_default => [0,100,0,0]}
|
24
|
+
# }]
|
25
|
+
# :options => {:title_padding => 20}
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# table hash explained:
|
29
|
+
# :title -> String - Title of the table
|
30
|
+
# :options -> Options applied to this table only
|
31
|
+
# :contents -> Array of Hashes of table contents. (this allows for dynamic resizing within a table to change the number
|
32
|
+
# of columns and headings without starting a new table)
|
33
|
+
# Table Hash:
|
34
|
+
# :headings -> Array of headings
|
35
|
+
# :rows -> Array of Arrays. Each array is a row
|
36
|
+
# :options -> Options applied to this set of contents only
|
37
|
+
#
|
38
|
+
# Heading and row values can be a string value, a Hash or nil. If the value is nil, it will be blackedout using
|
39
|
+
# the appropriate option for blacking out the cell. If it is a Hash, the :content key must be provided with the
|
40
|
+
# string value for the cell. Supported format for value Hashes:
|
41
|
+
#
|
42
|
+
# {:content => 'cell content', :color => [0,0,0,100], :background_color => [0,100,0,0], :font => ['Times', {:style => 'bold', :size => 10}]}
|
43
|
+
#
|
44
|
+
# Any or all of these values can by provided, with the exception of :content which MUST be provided. Any extra
|
45
|
+
# key/value pairs are ignored.
|
46
|
+
#
|
47
|
+
# Available options:
|
48
|
+
#
|
49
|
+
# :table_margin -> margin around entire all tables (the margin references the space around all tables. spacing between individual tables is set using :spacing)
|
50
|
+
# :table_margin_left -> left margin for all tables
|
51
|
+
# :table_margin_right -> right margin for all tables
|
52
|
+
# :table_margin_top -> top margin for all tables
|
53
|
+
# :table_margin_bottom -> bottom margin for all tables
|
54
|
+
# :default_font -> default font (defaults to first found font at size 6)
|
55
|
+
# :default_fill_color -> default fill color (defaults to black)
|
56
|
+
# :title_font -> font used for table title (defaults to first found font at size 18)
|
57
|
+
# :title_text_color -> text color for table title (defaults to white)
|
58
|
+
# :title_background_color -> background color for table title (defaults to blue)
|
59
|
+
# :title_text_alignment -> text alignment of title text (defaults :center)
|
60
|
+
# :title_padding -> extra padding around title text
|
61
|
+
# :headings_font -> default font used for cells in heading (defaults to first found font at font size 6)
|
62
|
+
# :headings_text_color_default -> default text color for cells in heading (defaults to black)
|
63
|
+
# :headings_text_colors -> text colors for cells in heading. zero based index corresponds to zero based column
|
64
|
+
# :headings_background_color_default -> default background color for heading cells (defaults to none)
|
65
|
+
# :headings_background_colors -> background colors for cells in heading. zero based index corresponds to zero based column
|
66
|
+
# :headings_blackout_color -> color to fill field when value does not exist (default to dark gray)
|
67
|
+
# :headings_text_alignment -> default text alignment for headings (defaults to center)
|
68
|
+
# :row_font -> default font used for cells in a row (defaults to first found font at font size 6)
|
69
|
+
# :row_text_color_default -> default text color for cells in a row (defaults to black)
|
70
|
+
# :row_text_colors -> text colors for cells in a row. zero based index corresponds to zero based column
|
71
|
+
# :row_background_color_default -> default background color for table cells (defaults to none)
|
72
|
+
# :row_background_colors -> background colors for cells in a row. zero based index corresponds to zero based column
|
73
|
+
# :row_text_alignment -> default text alignment for rows (defaults to center)
|
74
|
+
# :row_blackout_color -> color to fill field when value does not exist (defaults to dark gray)
|
75
|
+
# :border_color -> color of table border (defaults to black)
|
76
|
+
# :border_width -> width of table border
|
77
|
+
# :page_break_on_new_table -> move to a new page when a new table is started
|
78
|
+
# :show_title_after_page_break -> print table title after page break before continuing with table
|
79
|
+
# :show_headings_after_page_break -> print column headings after page break before continuing with data rows
|
80
|
+
# :new_page -> lambda called to start a new page
|
81
|
+
# :finalize_page -> lambda called before moving to the next page (example usage would be writing header/footer)
|
82
|
+
# :padding -> cell padding
|
83
|
+
# :spacing -> space between tables
|
84
|
+
# :y_initial -> initial y position when starting to write tables
|
85
|
+
# :page_width -> width of the page
|
86
|
+
# :page_height -> height of the page
|
87
|
+
# :break_for_min_width_on -> Array of single characters to break long strings on for wrapping
|
88
|
+
# :column_width_restrictions => {:fitted => [], :minimum => {}, :maximum => {}} # width restrictions (see extended explanation below)
|
89
|
+
#
|
90
|
+
# Width restrictions can be applied to columns to allow some control over column sizing. All
|
91
|
+
# restrictions work using a zero based index reference corresponding to table columns. For example:
|
92
|
+
# :fitted => [true, false, true]
|
93
|
+
# will make the first and third columns of the table fitted. Fitted columns will be as wide as the widest element and will not
|
94
|
+
# be expanded or shrunk when resizing. Minimum and maximum values can be set on columns as well:
|
95
|
+
# :minimum => {1 => 20, 4 => 40}, :maximum => {3 => 50}
|
96
|
+
# where the key is the column index and the value is the minimum/maximum number of points the column is allowed.
|
97
|
+
def simple_tables(tables, options={})
|
98
|
+
opts = st_defaults.merge(options)
|
99
|
+
new_page = opts[:new_page]
|
100
|
+
finalize_page = opts[:finalize_page]
|
101
|
+
opts.delete(:new_page)
|
102
|
+
opts.delete(:finalize_page)
|
103
|
+
break_page = lambda do
|
104
|
+
finalize_page.call
|
105
|
+
new_page.call
|
106
|
+
[opts[:xstart], opts[:ystart]]
|
107
|
+
end
|
108
|
+
started_opts = Marshal.load(Marshal.dump(opts)) # deep copy
|
109
|
+
new_opts_set = lambda{ Marshal.load(Marshal.dump(started_opts)) } # provide a copy when required
|
110
|
+
st_expand_options(opts)
|
111
|
+
tables = [tables] unless tables.is_a?(Array)
|
112
|
+
xpos = opts[:xstart]
|
113
|
+
ypos = opts[:y_initial] || opts[:ystart]
|
114
|
+
tables.each do |table|
|
115
|
+
opts = table[:options] ? new_opts_set.call.merge(table[:option]) : new_opts_set.call
|
116
|
+
st_expand_options(opts, table[:options] ? :nomargins : nil)
|
117
|
+
clean_table(table)
|
118
|
+
title = table[:title]
|
119
|
+
table[:contents] = [table[:contents]] unless table[:contents].is_a?(Array)
|
120
|
+
widths = st_find_column_widths(table[:contents].first, opts)
|
121
|
+
if((ypos - st_height_title(title, opts) - st_height_row(table[:contents].first[:headings], widths, opts, :headings) - st_height_row(table[:contents].first[:rows].first, widths, opts)) < opts[:yend])
|
122
|
+
xpos,ypos = break_page.call
|
123
|
+
end
|
124
|
+
xpos, ypos = st_write_title(title, [xpos,ypos], opts)
|
125
|
+
table[:contents].each do |row_collection|
|
126
|
+
if(row_collection[:options])
|
127
|
+
opts = opts.merge(row_collection[:options])
|
128
|
+
st_expand_options(opts, :nomargins)
|
129
|
+
end
|
130
|
+
widths = st_find_column_widths(row_collection, opts)
|
131
|
+
if((ypos - st_height_row(row_collection[:headings], widths, opts, :headings) - st_height_row(row_collection[:rows].first, widths, opts)) < opts[:yend])
|
132
|
+
xpos,ypos = break_page.call
|
133
|
+
xpos, ypos = st_write_title(title, [xpos,ypos], opts) if opts[:show_title_after_page_break]
|
134
|
+
end
|
135
|
+
xpos, ypos = st_write_row(row_collection[:headings], [xpos,ypos], widths, opts, :headings)
|
136
|
+
row_collection[:rows].each do |row|
|
137
|
+
if(ypos - st_height_row(row, widths, opts) < opts[:yend])
|
138
|
+
xpos,ypos = break_page.call
|
139
|
+
xpos,ypos = st_write_title(title, [xpos,ypos], opts) if opts[:show_title_after_page_break]
|
140
|
+
xpos,ypos = st_write_row(row_collection[:headings], [xpos,ypos], widths, opts, :headings) if opts[:show_headings_after_page_break]
|
141
|
+
end
|
142
|
+
xpos, ypos = st_write_row(row, [xpos,ypos], widths, opts)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
ypos -= opts[:spacing]
|
146
|
+
end
|
147
|
+
finalize_page.call
|
148
|
+
[xpos,ypos]
|
149
|
+
end
|
150
|
+
|
151
|
+
# table:: Hash of table contents
|
152
|
+
# Cleans up the table contents filling in missing parts like heading or row information.
|
153
|
+
# Will also check for column consistency within the rows.
|
154
|
+
def clean_table(table)
|
155
|
+
table[:contents] = [table[:contents]] unless table[:contents].is_a?(Array)
|
156
|
+
table[:contents].push [] if table[:contents].empty?
|
157
|
+
table[:contents].size.times do |idx|
|
158
|
+
table[:contents][idx] = {} unless table[:contents][idx].is_a?(Hash)
|
159
|
+
table[:contents][idx][:headings] = [] unless table[:contents][idx][:headings]
|
160
|
+
table[:contents][idx][:rows] = [[]] unless table[:contents][idx][:rows]
|
161
|
+
col_width = table[:contents][idx][:headings].size
|
162
|
+
table[:contents][idx][:rows].each do |row|
|
163
|
+
raise 'Improper number of columns used in row' if row.size != col_width && col_width > 0
|
164
|
+
col_width = row.size
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Set the default values for building the table. All of these can be overridden when
|
170
|
+
# calling #simple_tables
|
171
|
+
def st_defaults
|
172
|
+
default_margin = @default_margin || 0
|
173
|
+
default_font = @default_font || [font_families.keys.first, {:style => font_families.values.first.keys.first, :size => 6}]
|
174
|
+
{
|
175
|
+
:table_margin => default_margin, # margin around entire all tables (the margin references the space around all tables. spacing between individual tables is set using :spacing)
|
176
|
+
:table_margin_left => 0, # left margin for all tables
|
177
|
+
:table_margin_right => 0, # right margin for all tables
|
178
|
+
:table_margin_top => 0, # top margin for all tables
|
179
|
+
:table_margin_bottom => 0, # bottom margin for all tables
|
180
|
+
:default_font => default_font, # default font (defaults to first found font at size 6)
|
181
|
+
:default_fill_color => [0,0,0,100], # default fill color (defaults to black)
|
182
|
+
:title_font => [font_families.keys.first, {:style => font_families.values.first.keys.first, :size => 18}], # font used for table title (defaults to first found font at size 18)
|
183
|
+
:title_text_color => [0,0,0,0], # text color for table title (defaults to white)
|
184
|
+
:title_background_color => [100,100,0,0], # background color for table title (defaults to blue)
|
185
|
+
:title_text_alignment => :center, # text alignment of title text
|
186
|
+
:title_padding => 0, # extra padding around title text
|
187
|
+
:headings_font => default_font, # default font used for cells in heading (defaults to first found font at font size 6)
|
188
|
+
:headings_text_color_default => [0,0,0,100], # default text color for cells in heading (defaults to black)
|
189
|
+
:headings_text_colors => [], # text colors for cells in heading. zero based index corresponds to zero based column
|
190
|
+
:headings_background_color_default => nil, #default background color for heading cells (defaults to none)
|
191
|
+
:headings_background_colors => [], # background colors for cells in heading. zero based index corresponds to zero based column
|
192
|
+
:headings_blackout_color => [0,0,0,70], # color to fill field when value does not exist (default to dark gray)
|
193
|
+
:headings_text_alignment => :center, # default text alignment for headings (defaults to center)
|
194
|
+
:row_font => default_font, # default font used for cells in a row (defaults to first found found at font size 6)
|
195
|
+
:row_text_color_default => [0,0,0,100], # default text color for cells in a row (defaults to black)
|
196
|
+
:row_text_colors => [], # text colors for cells in a row. zero based index corresponds to zero based column
|
197
|
+
:row_background_color_default => nil, # default background color for table cells (defaults to none)
|
198
|
+
:row_background_colors => [], # background colors for cells in a row. zero based index corresponds to zero based column
|
199
|
+
:row_text_alignment => :center, # default text alignment for rows (defaults to center)
|
200
|
+
:row_blackout_color => [0,0,0,70], # color to fill field when value does not exist (defaults to dark gray)
|
201
|
+
:border_color => [0,0,0,100], # color of table border (defaults to black)
|
202
|
+
:border_width => 1, # width of table border
|
203
|
+
:page_break_on_new_table => true, # move to a new page when a new table is started
|
204
|
+
:show_title_after_page_break => true, # print table title after page break before continuing with table
|
205
|
+
:show_headings_after_page_break => true, # print column headings after page break before continuing with data rows
|
206
|
+
:column_width_restrictions => {:fitted => [], :minimum => {}, :maximum => {}}, # width restrictions (see extended explanation below)
|
207
|
+
# Width restrictions can be applied to columns to allow some control over column sizing. All
|
208
|
+
# restrictions work using a zero based index reference corresponding to table columns. For example:
|
209
|
+
# :fitted => [true, false, true]
|
210
|
+
# will make the first and third columns of the table fitted. Fitted columns will be as wide as the widest element and will not
|
211
|
+
# be expanded or shrunk when resizing. Minimum and maximum values can be set on columns as well:
|
212
|
+
# :minimum => {1 => 20, 4 => 40}, :maximum => {3 => 50}
|
213
|
+
# where the key is the column index and the value is the minimum/maximum number of points the column is allowed.
|
214
|
+
:new_page => lambda{ start_new_page }, # called to start a new page
|
215
|
+
:finalize_page => lambda{}, # called before moving to the next page (example usage would be writing header/footer)
|
216
|
+
:padding => 0, # cell padding
|
217
|
+
:spacing => 0, # space between tables
|
218
|
+
:y_initial => nil, # initial y position when starting to write tables
|
219
|
+
:page_width => bounds.width, # width of the page
|
220
|
+
:page_height => bounds.height, # height of the page
|
221
|
+
:break_for_min_width_on => [',','/',' '] # must be single characters
|
222
|
+
}
|
223
|
+
end
|
224
|
+
|
225
|
+
# opts:: Hash of current options
|
226
|
+
# Fills out the options hash based on current options
|
227
|
+
def st_expand_options(opts, *args)
|
228
|
+
unless(args.include?(:nomargins))
|
229
|
+
[:table_margin_left, :table_margin_right, :table_margin_top, :table_margin_bottom].each do |sym|
|
230
|
+
opts[sym] += opts[:table_margin]
|
231
|
+
end
|
232
|
+
opts[:table_width] = opts[:page_width] - opts[:table_margin_left] - opts[:table_margin_right]
|
233
|
+
opts[:ystart] = opts[:page_height] - opts[:table_margin_top] unless opts[:ystart]
|
234
|
+
opts[:xstart] = opts[:table_margin_left] unless opts[:xstart]
|
235
|
+
opts[:yend] = opts[:table_margin_bottom] unless opts[:yend]
|
236
|
+
opts[:xstop] = opts[:page_width] - opts[:table_margin_right]
|
237
|
+
end
|
238
|
+
opts[:column_width_restrictions][:fitted] = [] unless opts[:column_width_restrictions][:fitted]
|
239
|
+
opts[:column_width_restrictions][:minimum] = {} unless opts[:column_width_restrictions][:minimum]
|
240
|
+
opts[:column_width_restrictions][:maximum] = {} unless opts[:column_width_restrictions][:maximum]
|
241
|
+
end
|
242
|
+
|
243
|
+
# args:: Arguments returned for this method
|
244
|
+
# When called with no arguments, it returns current settings for
|
245
|
+
# things modified when building the table. When arguments are
|
246
|
+
# provided, it sets them. For example:
|
247
|
+
#
|
248
|
+
# original_settings = st_settings_helper
|
249
|
+
# # do some stuff here
|
250
|
+
# st_settings_helper(*original_settings)
|
251
|
+
def st_settings_helper(*args)
|
252
|
+
if(args.empty?)
|
253
|
+
orig_fill = fill_color
|
254
|
+
orig_stroke_color = stroke_color
|
255
|
+
orig_width = line_width
|
256
|
+
[orig_fill, orig_stroke_color, orig_width]
|
257
|
+
else
|
258
|
+
fill_color args[0]
|
259
|
+
stroke_color args[1]
|
260
|
+
line_width args[2]
|
261
|
+
args
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# row:: Row of data
|
266
|
+
# point:: x,y coordinate to start at
|
267
|
+
# opts:: Hash of options provided for building the table
|
268
|
+
# type:: Defaults to row. If not :row, assumes a heading is being written
|
269
|
+
# Writes a table row to the page
|
270
|
+
# Returns:: new x,y coordinate
|
271
|
+
def st_write_row(row, point, widths, opts, type=:row)
|
272
|
+
if(type != :row && row.empty?)
|
273
|
+
return point
|
274
|
+
end
|
275
|
+
xpos,ypos = point
|
276
|
+
startx = xpos
|
277
|
+
origs = st_settings_helper
|
278
|
+
height = st_height_row(row, widths, opts, type)
|
279
|
+
pad = opts[:padding]
|
280
|
+
row.each_with_index do |item,idx|
|
281
|
+
bounding_box [xpos,ypos], :height => height, :width => widths[idx] do
|
282
|
+
if(item.nil?)
|
283
|
+
fill_color opts["#{type}_blackout_color".to_sym]
|
284
|
+
fill_rectangle [0, bounds.height], bounds.width, bounds.height
|
285
|
+
else(item.is_a?(Hash))
|
286
|
+
item = {:content => item.to_s} unless item.is_a?(Hash)
|
287
|
+
background = item[:background_color] || opts["#{type}_background_colors".to_sym][idx] || opts["#{type}_background_color_default".to_sym]
|
288
|
+
if(background)
|
289
|
+
cur_fill = fill_color
|
290
|
+
fill_color background
|
291
|
+
fill_rectangle [0, bounds.height], bounds.width, bounds.height
|
292
|
+
fill_color cur_fill
|
293
|
+
end
|
294
|
+
cur_fill = fill_color
|
295
|
+
fill_color item[:color] || opts["#{type}_text_colors".to_sym][idx] || opts["#{type}_text_color_default".to_sym]
|
296
|
+
font *(item[:font] || opts["#{type}_font".to_sym]) do
|
297
|
+
alignment = item[:align] || opts["#{type}_text_alignment".to_sym]
|
298
|
+
boxed_width = bounds.width - (2 * pad)
|
299
|
+
boxed_height = bounds.height - (2 * pad)
|
300
|
+
text_height = height_of(item[:content], boxed_width)
|
301
|
+
valign_movement = text_height < boxed_height ? (boxed_height - text_height) / 2.0 : 0
|
302
|
+
bounding_box [pad, bounds.height - pad], :height => boxed_height, :width => boxed_width do
|
303
|
+
move_down valign_movement
|
304
|
+
text item[:content], :align => alignment
|
305
|
+
end
|
306
|
+
end
|
307
|
+
fill_color cur_fill
|
308
|
+
end
|
309
|
+
line_width opts[:border_width]
|
310
|
+
stroke_color opts[:border_color]
|
311
|
+
stroke_bounds
|
312
|
+
end
|
313
|
+
xpos += widths[idx]
|
314
|
+
end
|
315
|
+
st_settings_helper *origs
|
316
|
+
[startx,ypos - height]
|
317
|
+
end
|
318
|
+
|
319
|
+
# title:: String title for table
|
320
|
+
# point:: x,y coordinate to start printing
|
321
|
+
# opts:: Hash of options for table building
|
322
|
+
# Prints the table title
|
323
|
+
# Returns:: new x,y coordinate
|
324
|
+
def st_write_title(title, point, opts)
|
325
|
+
unless(title)
|
326
|
+
return point
|
327
|
+
end
|
328
|
+
xpos,ypos = point
|
329
|
+
origs = st_settings_helper
|
330
|
+
height = st_height_title(title, opts)
|
331
|
+
font *opts[:title_font] do
|
332
|
+
pad = opts[:title_padding] + opts[:padding]
|
333
|
+
bounding_box point, :width => opts[:table_width], :height => height do
|
334
|
+
fill_color opts[:title_background_color]
|
335
|
+
fill_rectangle [0, bounds.height], bounds.width, bounds.height
|
336
|
+
fill_color opts[:title_text_color]
|
337
|
+
boxed_width = bounds.width - (2 * pad)
|
338
|
+
boxed_height = bounds.height - (2 * pad)
|
339
|
+
text_height = height_of(title, boxed_width)
|
340
|
+
valign_movement = text_height < boxed_height ? (boxed_height - text_height) / 2.0 : 0
|
341
|
+
bounding_box [pad, bounds.height - pad], :height => boxed_height, :width => boxed_width do
|
342
|
+
move_down valign_movement
|
343
|
+
text title, :align => opts[:title_text_alignment]
|
344
|
+
end
|
345
|
+
line_width opts[:border_width]
|
346
|
+
stroke_color opts[:border_color]
|
347
|
+
stroke_bounds
|
348
|
+
end
|
349
|
+
end
|
350
|
+
st_settings_helper(*origs)
|
351
|
+
[xpos, ypos - height]
|
352
|
+
end
|
353
|
+
|
354
|
+
# table:: Hash of table contents
|
355
|
+
# opts:: Hash of options for building table
|
356
|
+
# Determine column widths for the table
|
357
|
+
def st_find_column_widths(table, opts)
|
358
|
+
info = {:widths => [], :min_widths => []}
|
359
|
+
([table[:headings]] + table[:rows]).each_with_index do |row, i|
|
360
|
+
type = i == 0 ? 'headings' : 'row'
|
361
|
+
row.each_with_index do |item, idx|
|
362
|
+
item = {:content => item.to_s} unless item.is_a?(Hash)
|
363
|
+
font *(item[:font] || opts["#{type}_font".to_sym]) do
|
364
|
+
string = item[:content].to_s
|
365
|
+
w = width_of(string) + (opts[:padding] * 2)
|
366
|
+
info[:widths][idx] = w if info[:widths][idx].nil? || info[:widths][idx] < w
|
367
|
+
biggest_word = string.tr(opts[:break_for_min_width_on].join(''), opts[:break_for_min_width_on].first).strip.split(opts[:break_for_min_width_on].first).max{|a,b|a.length <=> b.length}
|
368
|
+
m = (biggest_word ? width_of(biggest_word) : 0) + (opts[:padding] * 2)
|
369
|
+
info[:min_widths][idx] = m if info[:min_widths][idx].nil? || info[:min_widths][idx] < m
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
opts[:column_width_restrictions][:minimum].each_pair do |idx, val|
|
374
|
+
info[:widths][idx] = val if info[:widths][idx] < val && info[:min_widths][idx] < val
|
375
|
+
end
|
376
|
+
if(info[:widths].sum > opts[:table_width])
|
377
|
+
info = st_shrink_widths(info, opts)
|
378
|
+
elsif(info[:widths].sum < opts[:table_width] && info[:widths].sum > 0)
|
379
|
+
info = st_expand_widths(info, opts)
|
380
|
+
end
|
381
|
+
info[:widths]
|
382
|
+
end
|
383
|
+
|
384
|
+
# info:: Hash of width and minimum width values for table columns
|
385
|
+
# opts:: Hash of options for building the table
|
386
|
+
# Shrinks column widths until table fits within bounds. Will raise error
|
387
|
+
# if unable to fit table within bounds.
|
388
|
+
def st_shrink_widths(info, opts)
|
389
|
+
current_width = info[:widths].sum
|
390
|
+
reduce_by = current_width - opts[:table_width]
|
391
|
+
no_change = false
|
392
|
+
while(reduce_by > 0 && !no_change)
|
393
|
+
no_change = true
|
394
|
+
modable = {}
|
395
|
+
info[:widths].each_with_index do |w, idx|
|
396
|
+
modable[idx] = w if !opts[:column_width_restrictions][:fitted][idx] && w != opts[:column_width_restrictions][:minimum][idx]
|
397
|
+
end
|
398
|
+
modable.each_pair do |idx, w|
|
399
|
+
removal = reduce_by * (w / modable.values.sum)
|
400
|
+
unless(w <= info[:min_widths][idx])
|
401
|
+
hard_min = [opts[:column_width_restrictions][:minimum][idx].to_f, info[:min_widths][idx]].max
|
402
|
+
if(hard_min > w - removal && w > hard_min)
|
403
|
+
no_change = false
|
404
|
+
info[:widths][idx] = hard_min
|
405
|
+
elsif(hard_min < w - removal)
|
406
|
+
no_change = false
|
407
|
+
info[:widths][idx] = w - removal
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
current_width = info[:widths].sum
|
412
|
+
reduce_by = current_width - opts[:table_width]
|
413
|
+
end
|
414
|
+
if(no_change && reduce_by > 0)
|
415
|
+
raise "Failed to fit table data within boundaries. Too wide by: #{reduce_by} units"
|
416
|
+
else
|
417
|
+
info
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# info:: Hash of width and minimum width values for table columns
|
422
|
+
# opts:: Hash of options for building the table
|
423
|
+
# Expands column widths until table fills bounds. Will raise error if
|
424
|
+
# unable to expand columns enough to fit bounds (due to column size restrictions)
|
425
|
+
# TODO: add check for fitted columns. check for no change like we do when shrinking
|
426
|
+
def st_expand_widths(info, opts)
|
427
|
+
expand_by = opts[:table_width] - info[:widths].sum
|
428
|
+
while(expand_by > 0)
|
429
|
+
modable = {}
|
430
|
+
info[:widths].each_with_index do |w, idx|
|
431
|
+
modable[idx] = w if !opts[:column_width_restrictions][:fitted][idx] && w != opts[:column_width_restrictions][:maximum][idx]
|
432
|
+
end
|
433
|
+
extra_pad = expand_by / modable.size.to_f
|
434
|
+
modable.each_pair do |idx, w|
|
435
|
+
if(opts[:column_width_restrictions][:maximum][idx] && opts[:column_width_restrictions][:maximum][idx] < w + extra_pad)
|
436
|
+
info[:widths][idx] = opts[:column_width_restrictions][:maximum][idx]
|
437
|
+
else
|
438
|
+
info[:widths][idx] = w + extra_pad
|
439
|
+
end
|
440
|
+
end
|
441
|
+
expand_by = opts[:table_width] - info[:widths].sum
|
442
|
+
end
|
443
|
+
info
|
444
|
+
end
|
445
|
+
|
446
|
+
# title:: String
|
447
|
+
# opts:: Hash of options for building the table
|
448
|
+
# Returns the height of the title
|
449
|
+
def st_height_title(title, opts)
|
450
|
+
h = 0
|
451
|
+
font *opts[:title_font] do
|
452
|
+
h = height_of(title.to_s, opts[:table_width]) + (opts[:padding] * 2) + (opts[:title_padding] * 2)
|
453
|
+
end
|
454
|
+
h + 2 # bit of safety to keep from busting the cell
|
455
|
+
end
|
456
|
+
|
457
|
+
# row:: Array of row values
|
458
|
+
# widths:: Array of column width values
|
459
|
+
# opts:: Hash of options for building the table
|
460
|
+
# type:: row or heading
|
461
|
+
# Returns the max height for the given row
|
462
|
+
def st_height_row(row, widths, opts, type=:row)
|
463
|
+
return 0 if row.nil? || row.empty?
|
464
|
+
h = 0
|
465
|
+
row.each_with_index do |item, idx|
|
466
|
+
test_height = 0
|
467
|
+
item = {:content => item.to_s} unless item.is_a?(Hash)
|
468
|
+
font *(item[:font] || opts["#{type}_font".to_sym]) do
|
469
|
+
test_height = height_of(item[:content].to_s, widths[idx] - (opts[:padding] * 2))
|
470
|
+
end
|
471
|
+
h = test_height if h < test_height
|
472
|
+
end
|
473
|
+
h + (opts[:padding] * 2) + 2 # bit of safety to keep from busting the cell
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
data/lib/rabidprawns.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
|
3
|
+
class RabidPrawns
|
4
|
+
class << self
|
5
|
+
# Returns array of available rabids for loading
|
6
|
+
def available_rabids
|
7
|
+
libs = []
|
8
|
+
Dir.new(File.expand_path(File.expand_path(__FILE__) + '/../rabidprawns')).each do |item|
|
9
|
+
next if %w(. ..).include? item
|
10
|
+
libs << item.gsub('.rb', '').to_sym
|
11
|
+
end
|
12
|
+
libs
|
13
|
+
end
|
14
|
+
|
15
|
+
# args:: rabid symbols
|
16
|
+
# Load the given rabids
|
17
|
+
def load_rabid(*args)
|
18
|
+
list = self.available_rabids
|
19
|
+
args.each do |arg|
|
20
|
+
raise ArgumentError.new "Unknown rabid requested: #{arg}" unless list.include?(arg)
|
21
|
+
require "rabidprawns/#{arg}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :load_rabids :load_rabid
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Prawn
|
30
|
+
class Document
|
31
|
+
def height_of(text, args)
|
32
|
+
unless(args.is_a?(Hash))
|
33
|
+
args = {:width => args}
|
34
|
+
end
|
35
|
+
super(text, args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/rabidprawns.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rabidprawns'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.summary = 'Rabid Prawns'
|
5
|
+
s.author = 'Chris Roberts'
|
6
|
+
s.email = 'chrisroberts.code@gmail.com'
|
7
|
+
s.homepage = 'http://github.com/chrisroberts/rabidprawns'
|
8
|
+
s.description = 'Rabid Prawns'
|
9
|
+
s.require_path = 'lib'
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.extra_rdoc_files = ['README.rdoc']
|
12
|
+
s.files = %w(lib/rabidprawns.rb lib/rabidprawns/simple_tables.rb lib/rabidprawns/columned_page.rb README.rdoc rabidprawns.gemspec)
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rabidprawns
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Chris Roberts
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-08-04 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Rabid Prawns
|
23
|
+
email: chrisroberts.code@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- lib/rabidprawns.rb
|
32
|
+
- lib/rabidprawns/simple_tables.rb
|
33
|
+
- lib/rabidprawns/columned_page.rb
|
34
|
+
- README.rdoc
|
35
|
+
- rabidprawns.gemspec
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://github.com/chrisroberts/rabidprawns
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
hash: 3
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.3.7
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: Rabid Prawns
|
70
|
+
test_files: []
|
71
|
+
|