dynamic_images 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +27 -0
- data/README.rdoc +104 -0
- data/README_USAGE.rdoc +67 -0
- data/Rakefile +14 -0
- data/dynamic_images.gemspec +35 -0
- data/examples/gtk_window.rb +39 -0
- data/examples/named_colors_table.rb +13 -0
- data/init.rb +6 -0
- data/lib/dynamic_image.rb +240 -0
- data/lib/elements/block_element.rb +224 -0
- data/lib/elements/element_interface.rb +320 -0
- data/lib/elements/image_element.rb +82 -0
- data/lib/elements/table_cell_element.rb +34 -0
- data/lib/elements/table_element.rb +220 -0
- data/lib/elements/text_element.rb +195 -0
- data/lib/parsers/xml.dtd +154 -0
- data/lib/parsers/xml_parser.rb +135 -0
- data/lib/render_image.rb +60 -0
- data/lib/sources/color_source.rb +97 -0
- data/lib/sources/gradient_source.rb +200 -0
- data/lib/sources/source_factory.rb +36 -0
- data/test/asym.rb +54 -0
- data/test/performance.1.rb +86 -0
- data/test/performance.2.rb +159 -0
- data/test/performance.3.rb +236 -0
- data/test/performance.rb +51 -0
- data/test/units1.rb +185 -0
- data/test/units2.rb +162 -0
- metadata +115 -0
@@ -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
|