dynamic_images 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
1
+ require File.dirname(__FILE__) + '/element_interface.rb'
2
+
3
+ module DynamicImageElements
4
+ # Element providing rendering of image.
5
+ class ImageElement
6
+ include ElementInterface
7
+
8
+ # Image element accepts source as path to image file and options +Hash+.
9
+ #
10
+ # === Options
11
+ # Options can contain general attributes specified by BlockElement if it's created by it.
12
+ #
13
+ # [:alpha]
14
+ # Makes image semi-transparent. Valid values are 0.0 - 1.0 or "0%" - "100%". Default is 1.0.
15
+ # [:crop]
16
+ # Sets cropping rectangle by values in this order: [x, y, width, height]. Use +Array+ or +String+ to describe it, values in +String+ must be separated by space char.
17
+ # [:height]
18
+ # Sets height of image.
19
+ # [:width]
20
+ # Sets width of image.
21
+ #
22
+ def initialize(source, options, parent)
23
+ @source = source
24
+ @options = options
25
+ @parent = parent
26
+ use_options :margin
27
+ if @options[:crop].is_a? Array
28
+ @crop = (@options[:crop].flatten.map{|v| v.class == Fixnum || v.class == Float || v.class == String ? v.to_i : 0} + [0, 0, 0, 0])[0..3]
29
+ else
30
+ @crop = (@options[:crop].to_s.scan(/\-?\d+/).flatten.map(&:to_i) + [0, 0, 0, 0])[0..3]
31
+ end
32
+ end
33
+
34
+ private
35
+ def image
36
+ unless @image
37
+ if @source.to_s =~ /\.png$/i
38
+ @image = Cairo::ImageSurface.from_png @source
39
+ else
40
+ if defined? Gdk
41
+ @image = Gdk::Pixbuf.new @source
42
+ else
43
+ raise "Unsupported source format of: #{@source}"
44
+ end
45
+ end
46
+ end
47
+ @image
48
+ end
49
+
50
+ def inner_size
51
+ size = [0, 0]
52
+ unless @options[:width] && @options[:height]
53
+ size = [image.width, image.height]
54
+ end
55
+ size[0] = @crop[2] if @crop[2] > 0
56
+ size[1] = @crop[3] if @crop[3] > 0
57
+ size[0] = @options[:width] if @options[:width]
58
+ size[1] = @options[:height] if @options[:height]
59
+ size
60
+ end
61
+
62
+ def draw(x, y, endless)
63
+ w, h = element_size
64
+ imgsize = [image.width, image.height]
65
+ imgsize[0] = @crop[2] if @crop[2] > 0
66
+ imgsize[1] = @crop[3] if @crop[3] > 0
67
+ scale = [w.to_f/imgsize[0].to_f, h.to_f/imgsize[1].to_f]
68
+ context.scale *scale
69
+ context.save
70
+ if image.is_a? Cairo::Surface
71
+ context.set_source image, x/scale[0]-@crop[0], y/scale[1]-@crop[1]
72
+ else
73
+ context.set_source_pixbuf image, x/scale[0]-@crop[0], y/scale[1]-@crop[1]
74
+ end
75
+ context.rectangle x/scale[0], y/scale[1], w/scale[0], h/scale[1]
76
+ context.clip
77
+ context.paint @options[:alpha]
78
+ context.restore
79
+ context.scale 1.0/scale[0], 1.0/scale[1]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/block_element.rb'
2
+
3
+ module DynamicImageElements
4
+ # Element is used for compositing another elements in it. It's inherited from BlockElement and can be used in same way.
5
+ class TableCellElement < BlockElement
6
+
7
+ # Table cell element accepts options +Hash+. Block can be given and class provides element self to composite elements in it.
8
+ #
9
+ # See BlockElement.new for more information
10
+ #
11
+ # === Options
12
+ # Options are same as for BlockElement. You can also use further options to set cell behavior.
13
+ #
14
+ # [:colspan]
15
+ # Sets number of columns which this cells takes. Don't create next cells for already taken space.
16
+ # [:rowspan]
17
+ # Sets number of rows which this cells takes. Don't create next cells for already taken space.
18
+ # [:width]
19
+ # Same as described in BlockElement. You can also set <tt>"0%"</tt> to fit all remaining space and ather elements will fit to its minimum size.
20
+ #
21
+ def initialize(options, parent = nil, &block) # :yields: block_element
22
+ super options, parent, &block
23
+ end
24
+
25
+ # Gets width for inner elements
26
+ def width #:nodoc:
27
+ @options[:width]
28
+ end
29
+ # Gets height for inner elements
30
+ def height #:nodoc:
31
+ @options[:height]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,220 @@
1
+ require File.dirname(__FILE__) + '/element_interface.rb'
2
+ require File.dirname(__FILE__) + '/table_cell_element.rb'
3
+
4
+ module DynamicImageElements
5
+ # Element is used for creating a table structure. It is composition of cells and rows of table.
6
+ class TableElement
7
+ include ElementInterface
8
+
9
+ # Table element accepts options +Hash+. Block can be given and class provides element self to composite cells in it.
10
+ #
11
+ # You can create table structure by two ways. By basic hierarchy where cells are nested in row block or set columns number by :cols option and use cells only. You can also combine these two ways by using row method to wrap current row before reaching its end.
12
+ #
13
+ # === Example
14
+ # Using basic hierarchy:
15
+ # table do
16
+ # row do
17
+ # cell do
18
+ # text "cell 1 in row 1"
19
+ # end
20
+ # cell do
21
+ # text "cell 2 in row 1"
22
+ # end
23
+ # end
24
+ # row do
25
+ # cell do
26
+ # text "cell 1 in row 2"
27
+ # end
28
+ # cell do
29
+ # text "cell 2 in row 2"
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ # Using :cols option:
35
+ # table :cols => 2 do
36
+ # cell do
37
+ # text "cell 1 in row 1"
38
+ # end
39
+ # cell do
40
+ # text "cell 2 in row 1"
41
+ # end
42
+ #
43
+ # cell do
44
+ # text "cell 1 in row 2"
45
+ # end
46
+ # cell do
47
+ # text "cell 2 in row 2"
48
+ # end
49
+ # end
50
+ #
51
+ # === Options
52
+ # You can use also aliases provided by ElementInterface::OPTIONS_ALIASES in all options +Hash+es.
53
+ #
54
+ # [:background]
55
+ # Described in BlockElement.
56
+ # [:border]
57
+ # Described in BlockElement.
58
+ # [:cols]
59
+ # Sets number of columns in table structure. If value is given it will automatically sort cells to rows.
60
+ #
61
+ # You can also manually wrap the row by calling row method between cells.
62
+ #
63
+ def initialize(options, parent = nil, &block) # :yields: table_element
64
+ @options = options
65
+ @parent = parent
66
+ @elements = []
67
+ @cells_map = [[]]
68
+ @map_pos = [0, 0]
69
+ use_options :margin
70
+ @cols = options[:cols] ? options[:cols].to_i : nil
71
+ process self, &block if block
72
+ end
73
+
74
+ private
75
+ def inner_size
76
+ unless @size
77
+ size = [0, 0]
78
+ # set width of cols and height of rows
79
+ # get size of cells
80
+ [0, 1].each do |dimension|
81
+ size_key = [:width, :height][dimension]
82
+ table_key = [:cols, :rows][dimension]
83
+ sizes = []
84
+ size_types = []
85
+ @cells_map.each_with_index do |cells_row, row_index|
86
+ cells_row.each_with_index do |element, col_index|
87
+ index = [col_index, row_index][dimension]
88
+ sizes[index] = {:max => 0, :sum => 0, :count => 0, :percentage => 0, :fixed => 0, :blow_up => false} unless sizes[index]
89
+ size_types[index] = :dynamic unless size_types[index]
90
+ size_of_element = element[:obj].final_size[dimension] / element[table_key]
91
+ sizes[index][:max] = size_of_element if size_of_element > sizes[index][:max]
92
+ sizes[index][:sum] = sizes[index][:sum] + size_of_element
93
+ sizes[index][:count] = sizes[index][:count] + 1
94
+ sizes[index][:percentage] = element[size_key] if element[size_key].class == Float
95
+ sizes[index][:fixed] = element[size_key] if element[size_key].class == Fixnum
96
+ sizes[index][:blow_up] = true if element[size_key].class == String
97
+ end
98
+ end
99
+ size[dimension] = @options[size_key] ? @options[size_key] : sizes.map{|s| s[:max]}.inject(:+).to_i
100
+ # calculating new size
101
+ # first step: sets basically known values
102
+ sizes.each_with_index do |s, index|
103
+ if s[:blow_up]
104
+ sizes[index] = 0
105
+ elsif s[:fixed] > 0
106
+ sizes[index] = s[:fixed]
107
+ size_types[index] = :fixed
108
+ elsif s[:percentage] > 0
109
+ sizes[index] = size[dimension] * s[:percentage]
110
+ elsif !@options[size_key]
111
+ sizes[index] = s[:max]
112
+ else
113
+ sizes[index][:avg] = s[:sum] / s[:count]
114
+ end
115
+ end
116
+ # second step: resize zero value to all free space (it's 0% effect)
117
+ remaining_size = nil
118
+ zeros_count = nil
119
+ sizes.each_with_index do |s, index|
120
+ next unless s.class == Fixnum && s == 0
121
+ remaining_size = [size[dimension] - sizes.map{|siz| siz.is_a?(Hash) ? siz[:avg] : siz }.inject(:+).to_i, 0].max unless remaining_size
122
+ zeros_count = sizes.select{|siz| siz.class == Fixnum && siz == 0}.size unless zeros_count
123
+ sizes[index] = remaining_size/zeros_count
124
+ end
125
+ # third step: split free space to remaining cells by average size
126
+ remaining_size = nil
127
+ avgs_sum = nil
128
+ sizes.each_with_index do |s, index|
129
+ next unless s.is_a? Hash
130
+ remaining_size = [size[dimension] - sizes.select{|siz| siz.class == Fixnum}.inject(:+).to_i, 0].max unless remaining_size
131
+ avgs_sum = [sizes.select{|siz| siz.is_a?(Hash)}.map{|siz| siz[:avg]}.inject(:+), 1].max unless avgs_sum
132
+ sizes[index] = [remaining_size*s[:avg]/avgs_sum, 1].max
133
+ end
134
+ # resize cell
135
+ @cells_map.each_with_index do |cells_row, row_index|
136
+ cells_row.each_with_index do |element, col_index|
137
+ index = [col_index, row_index][dimension]
138
+ unless element[:is_duplicit]
139
+ s = sizes[index..index+element[table_key]-1].inject(:+).to_i
140
+ if dimension == 0
141
+ element[:obj].set_width s, (size_types[index] == :fixed)
142
+ else
143
+ element[:obj].set_height s, (size_types[index] == :fixed)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ @size = size
150
+ end
151
+ @size
152
+ end
153
+
154
+ def draw(x, y, endless)
155
+ draw_background x, y
156
+ draw_border x, y
157
+ inner_size
158
+ @elements.each do |element|
159
+ x_pos = element[:x].class == Proc ? element[:x].call : element[:x]
160
+ y_pos = element[:y].class == Proc ? element[:y].call : element[:y]
161
+ element[:obj].draw! x_pos+x, y_pos+y
162
+ end
163
+ @drawing = false
164
+ end
165
+
166
+ def add_element(e, options)
167
+ @size = nil
168
+ cols = [options[:colspan].to_i, 1].max
169
+ rows = [options[:rowspan].to_i, 1].max
170
+ move_to_next_pos
171
+ on_left_element = @map_pos[0] == 0 ? nil : @cells_map[@map_pos[1]][@map_pos[0]-1]
172
+ x = on_left_element ? lambda{(on_left_element[:x].class == Proc ? on_left_element[:x].call : on_left_element[:x]) + on_left_element[:obj].final_size[0]} : 0
173
+ on_top_element = @map_pos[1] == 0 ? nil : @cells_map[@map_pos[1]-1][@map_pos[0]]
174
+ y = on_top_element ? lambda{(on_top_element[:y].class == Proc ? on_top_element[:y].call : on_top_element[:y]) + on_top_element[:obj].final_size[1]} : 0
175
+ element = {:x => x, :y => y, :width => options[:width], :height => options[:height], :obj => e, :cols => cols, :rows => rows, :is_duplicit => false}
176
+ e.set_width(width ? (width * options[:width]).to_i : nil, false) if options[:width].class == Float
177
+ e.set_height(height ? (height * options[:height]).to_i : nil, false) if options[:height].class == Float
178
+ @elements << element
179
+ @map_pos[0].upto(@map_pos[0]+cols-1) do |x|
180
+ @map_pos[1].upto(@map_pos[1]+rows-1) do |y|
181
+ @cells_map[y] ||= []
182
+ @cells_map[y][x] = element
183
+ unless element[:is_duplicit]
184
+ element = element.clone
185
+ element[:is_duplicit] = true
186
+ end
187
+ end
188
+ end
189
+ e
190
+ end
191
+
192
+ def move_to_next_pos
193
+ @map_pos[0] = @map_pos[0] + 1 while @cells_map[@map_pos[1]][@map_pos[0]]
194
+ row if @cols && @map_pos[0] >= @cols
195
+ end
196
+
197
+ public
198
+ # Creates new TableCellElement as a cell of tables composite. See TableCellElement.new for arguments information.
199
+ def cell(options = {}, &block) # :yields: cell_element
200
+ zero_percent = {}
201
+ zero_percent[:width] = "0%" if options[:width] == "0%"
202
+ zero_percent[:height] = "0%" if options[:height] == "0%"
203
+ treat_options options
204
+ add_element TableCellElement.new(options, self, &block), options.merge(zero_percent)
205
+ end
206
+
207
+ # Creates row of cells and gives block for compositing inner elements in table.
208
+ #
209
+ # You can also call it alone to make next cells in new row.
210
+ def row(&block) # :yields: table_element
211
+ process self, &block if block
212
+ if @map_pos[0] > 0
213
+ @map_pos = [0, @map_pos[1] + 1]
214
+ @cells_map[@map_pos[1]] ||= []
215
+ move_to_next_pos
216
+ end
217
+ self
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,195 @@
1
+ require File.dirname(__FILE__) + '/element_interface.rb'
2
+
3
+ module DynamicImageElements
4
+ # Element providing drawing of stylized text. You can use markup language of text specified by http://developer.gnome.org/pango/stable/PangoMarkupFormat.html
5
+ class TextElement
6
+ include ElementInterface
7
+
8
+ # Text element accepts content as text and options +Hash+. Block can be given and class provides original Pango::Layout object to modify it.
9
+ #
10
+ # === Options
11
+ # Options can contain general attributes specified by BlockElement if it's created by it.
12
+ # Most of TextElement options are based on Pango::Layout object.
13
+ #
14
+ # [:align]
15
+ # Alignment of paragraphs. Valid values are :left, :center and :right.
16
+ # [:auto_dir]
17
+ # If true, compute the bidirectional base direction from the layout's contents.
18
+ # [:color]
19
+ # Sets foreground of text element. Accepts value for DynamicImageSources::SourceFactory.
20
+ # [:crop_to]
21
+ # Crop text to reach a specified size. Use an Array or String separated by space chars to provide further arguments.
22
+ #
23
+ # Valid crop_to methods are :letters, :words and :sentences. Default method is :words if no one is given. Sentence is determined by one of ".!?".
24
+ #
25
+ # Add :lines (or :line) as second argument if size is for lines number, not by method. Lines are determined by parent container or :width if it's given. See examples for more details.
26
+ #
27
+ # ==== Examples
28
+ # * <tt>:crop_to => 10</tt> will crop text down to 10 words
29
+ # * <tt>:crop_to => [10, :letters]</tt> will crop text down to 10 letters and it's same as <tt>:crop => "10 letters"</tt>
30
+ # * <tt>:crop_to => [3, :lines]</tt> will crop text down by words to 3 lines
31
+ # * <tt>:crop_to => [1, :line, :letters]</tt> will crop text down by letters to 1 line
32
+ #
33
+ # [:crop_suffix]
34
+ # It's value is added at end of text in case it's cropped. It can be caused by :crop and :to_fit options.
35
+ # [:font]
36
+ # Creates a new font description from a string representation in the form "[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]", where FAMILY-LIST is a comma separated list of families optionally terminated by a comma, STYLE_OPTIONS is a whitespace separated list of words where each WORD describes one of style, variant, weight, or stretch, and SIZE is an decimal number (size in points). Any one of the options may be absent. If FAMILY-LIST is absent, then the family_name field of the resulting font description will be initialized to nil. If STYLE-OPTIONS is missing, then all style options will be set to the default values. If SIZE is missing, the size in the resulting font description will be set to 0. If str is nil, creates a new font description structure with all fields unset.
37
+ # [:indent]
38
+ # Sets the width to indent each paragraph.
39
+ # [:justify]
40
+ # Sets whether or not each complete line should be stretched to fill the entire width of the layout. This stretching is typically done by adding whitespace, but for some scripts (such as Arabic), the justification is done by extending the characters.
41
+ # [:spacing]
42
+ # Sets the amount of spacing between the lines of the layout.
43
+ # [:to_fit]
44
+ # Sets method how to deform text to fit its parent element. You can use an Array or String separated by space chars to provide further arguments.
45
+ #
46
+ # Valid values are :crop and :resize.
47
+ #
48
+ # Further argument for :crop are method of cropping (:letters, :words, :sentences). Default method is :words if no one is given. Sentence is determined by one of ".!?". You can specify text to add at end of text if it's cropped by :crop_suffix.
49
+ #
50
+ # You can combine methods in your own order by setting further method in next positions of Array or String. Stop value must be set between methods to determine when to use next method.
51
+ #
52
+ # ==== Example
53
+ # For one method use:
54
+ # * <tt>:to_fit => :crop</tt> is same as <tt>:to_fit => [:crop]</tt> and <tt>:to_fit => "crop"</tt>
55
+ # * <tt>:to_fit => :resize</tt>
56
+ # * <tt>:to_fit => [:crop, :letters]</tt> will crop text by letters to fit its parent container and its same as <tt>:to_fit => "crop letters"</tt>
57
+ # For more methods use:
58
+ # * <tt>:to_fit => [:crop, 10, :resize]</tt> will crop down to 10 words to fit, if it's not enough it will reduce size of font
59
+ # * <tt>:to_fit => [:crop, :letters, 10, :resize]</tt> will crop down to 10 letters to fit, if it's not enough it will reduce size of font
60
+ # * <tt>:to_fit => [:resize, 6, :crop]</tt> will reduce size down to 6 pt letters to fit, if it's not enough it will crop words
61
+ # * <tt>:to_fit => [:resize, 6, :crop, :letters]</tt> will reduce size down to 6 pt letters to fit, if it's not enough it will crop letters
62
+ # * <tt>:to_fit => [:crop, 2, :resize, 8, :crop, :letters]</tt> will crop text down to 2 words, if it's not enough to fit it will resize font down, but only to 8 pt and if it's still not enough it will continue with cropping text by letters
63
+ #
64
+ def initialize(content, options, parent, &block) # :yields: pango_layout
65
+ @content = content
66
+ @options = options
67
+ @parent = parent
68
+ @block = block
69
+ use_options :margin
70
+ end
71
+
72
+ private
73
+ # Tolerance of space to drawing because <tt>Pango::Layout</tt> doesn't fix exactly to given size
74
+ SIZE_TOLERANCE = 0
75
+ def setup_pango_layout(pango_layout)
76
+ pango_layout.set_width((@parent.width-@margin[1]-@margin[3]+SIZE_TOLERANCE)*Pango::SCALE) if @parent.width
77
+ pango_layout.set_font_description Pango::FontDescription.new(@options[:font].to_s) if @options[:font]
78
+ pango_layout.set_width @options[:width]*Pango::SCALE if @options[:width]
79
+ pango_layout.set_alignment({:left => Pango::ALIGN_LEFT, :center => Pango::ALIGN_CENTER, :right => Pango::ALIGN_RIGHT}[@options[:align].to_sym]) if @options[:align]
80
+ pango_layout.set_indent @options[:indent].to_i*Pango::SCALE if @options[:indent] && (@options[:indent].class == Fixnum || @options[:indent].class == Float || @options[:indent].class == String)
81
+ pango_layout.set_spacing @options[:spacing].to_i*Pango::SCALE if @options[:spacing] && (@options[:spacing].class == Fixnum || @options[:spacing].class == Float || @options[:spacing].class == String)
82
+ pango_layout.set_justify !!@options[:justify] if @options[:justify]
83
+ pango_layout.set_auto_dir !!@options[:auto_dir] if @options[:auto_dir]
84
+ attrs, txt = Pango.parse_markup @content.to_s
85
+ pango_layout.set_attributes attrs
86
+ pango_layout.set_text txt
87
+ #crop_to option
88
+ if @options[:crop_to]
89
+ option = @options[:crop_to].is_a?(Array) ? @options[:crop_to].clone.flatten : @options[:crop_to].to_s.downcase.strip.split(/\s+/)
90
+ stop_value = option.shift.to_i
91
+ lines_unit = option[0].to_s == 'lines' || option[0].to_s == 'line'
92
+ option.shift if lines_unit
93
+ suffix = @options[:crop_suffix].to_s
94
+ split = /\s+/ #words
95
+ split = /[\.!\?]+/ if option.first.to_s == "sentences"
96
+ split = // if option.first.to_s == "letters"
97
+ count = txt.strip.split(split).size
98
+ loop do
99
+ break if (lines_unit && pango_layout.line_count <= stop_value) || txt == suffix
100
+ break if !lines_unit && count <= stop_value
101
+ txt = crop txt, option.first
102
+ count -= 1
103
+ pango_layout.set_text txt+suffix
104
+ end
105
+ end
106
+ #to_fit option
107
+ if @options[:to_fit]
108
+ width = (@options[:width] || (@parent.width ? @parent.width-@margin[1]-@margin[3] : nil)).to_i
109
+ height = (@parent.height ? @parent.height-@margin[0]-@margin[2] : nil).to_i
110
+ if width > 0 || height > 0
111
+ suffix = @options[:crop_suffix].to_s
112
+ option = @options[:to_fit].is_a?(Array) ? @options[:to_fit].clone.flatten : @options[:to_fit].to_s.downcase.strip.split(/\s+/)
113
+ methods = [] #it should look like this [:method1, :method2, ..., :methodN]
114
+ method_args = [] #it should look like this [[arg1, arg2, ..., stop_value1], [arg1, ..., stop_value2], ..., [arg1, ...]]
115
+ option.each do |opt|
116
+ if [:crop, :resize].include? opt.to_s.to_sym
117
+ methods << opt.to_sym
118
+ method_args << []
119
+ else
120
+ method_args.last << opt if method_args.last
121
+ end
122
+ end
123
+ methods.each do |method|
124
+ case method
125
+ when :crop
126
+ split = /\s+/ #words
127
+ split = /[\.!\?]+/ if method_args.first.first == "sentences"
128
+ split = // if method_args.first.first == "letters"
129
+ count = txt.strip.split(split).size
130
+ while (width > 0 && pango_layout.size[0]/Pango::SCALE > width+SIZE_TOLERANCE || height > 0 && pango_layout.size[1]/Pango::SCALE > height+SIZE_TOLERANCE) && count > method_args.first.last.to_s.to_i
131
+ txt = crop txt, method_args.first.first
132
+ count -= 1
133
+ pango_layout.set_text txt+suffix
134
+ end
135
+ when :resize
136
+ pango_layout.set_font_description Pango::FontDescription.new unless pango_layout.font_description
137
+ font_size = pango_layout.font_description.size
138
+ font_size = 13 if font_size.zero?
139
+ while (width > 0 && pango_layout.size[0]/Pango::SCALE > width+SIZE_TOLERANCE/2 || height > 0 && pango_layout.size[1]/Pango::SCALE > height+SIZE_TOLERANCE/2) && font_size > 1 && font_size > method_args.first.last.to_s.to_i
140
+ pango_layout.set_font_description pango_layout.font_description.set_size((font_size -= 1)*Pango::SCALE)
141
+ end
142
+ end
143
+ method_args.shift
144
+ end
145
+ end
146
+ end
147
+ process pango_layout, &@block if @block
148
+ pango_layout
149
+ end
150
+
151
+ def crop(txt, method)
152
+ case method.to_s
153
+ when 'sentences'
154
+ txt.sub(/([^\.!\?]+|[^\.!\?]+[\.!\?]+)$/, '')
155
+ when 'letters'
156
+ txt.sub(/[\S\s]$/, '')
157
+ else #words
158
+ txt.sub(/\s*\S+\s*$/, '')
159
+ end
160
+ end
161
+
162
+ def inner_size
163
+ unless @size
164
+ if context
165
+ pango_layout = context.create_pango_layout
166
+ else
167
+ tmp_surface = Cairo::ImageSurface.new 1, 1
168
+ tmp_context = Cairo::Context.new tmp_surface
169
+ pango_layout = tmp_context.create_pango_layout
170
+ end
171
+ setup_pango_layout pango_layout
172
+ size = pango_layout.size.map{|i| i/Pango::SCALE}
173
+ unless context
174
+ tmp_context.destroy
175
+ tmp_surface.destroy
176
+ end
177
+ @size = size
178
+ end
179
+ @size
180
+ end
181
+
182
+ def draw(x, y, endless) #:nodoc:
183
+ if @options[:color]
184
+ color_x = x
185
+ width = nil
186
+ width = @parent.width-@margin[1]-@margin[3]+SIZE_TOLERANCE if @parent.width
187
+ width = @options[:width] if @options[:width]
188
+ color_x = x + (width-inner_size[0])/2 if width
189
+ @options[:color].set_source *[context, color_x, y, inner_size].flatten
190
+ end
191
+ context.move_to x, y
192
+ context.show_pango_layout setup_pango_layout(context.create_pango_layout)
193
+ end
194
+ end
195
+ end