rabidprawns 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|